From 41f99cae366981108d405e9e871f79751fa4e5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 12 Feb 2026 11:36:02 +0100 Subject: [PATCH 01/72] Basic python setup for mapping script --- scripts/config_mapping/Readme.md | 29 ++++++ scripts/config_mapping/lifecycle_config.py | 90 +++++++++++++++++++ scripts/config_mapping/requirements.txt | 2 + scripts/config_mapping/tests.py | 74 +++++++++++++++ scripts/config_mapping/tests/.gitignore | 1 + .../basic_test/expected_output/hm_demo.json | 1 + .../basic_test/expected_output/lm_demo.json | 1 + .../basic_test/input/test_input_basic.json | 24 +++++ 8 files changed, 222 insertions(+) create mode 100644 scripts/config_mapping/Readme.md create mode 100644 scripts/config_mapping/lifecycle_config.py create mode 100644 scripts/config_mapping/requirements.txt create mode 100644 scripts/config_mapping/tests.py create mode 100644 scripts/config_mapping/tests/.gitignore create mode 100644 scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json create mode 100644 scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json create mode 100644 scripts/config_mapping/tests/basic_test/input/test_input_basic.json diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md new file mode 100644 index 00000000..5c1ef5d7 --- /dev/null +++ b/scripts/config_mapping/Readme.md @@ -0,0 +1,29 @@ +# Motivation + +We are introducing a new, simpler configuration file for the launch_manager. +To make use of the new configuration as early as possible, we are introducing a script to map the new configuration to the old configuration. +Once the source code of the launch_manager has been adapted to read in the new configuration file, the mapping script will become obsolete. + +# Usage + +Providing a json file using the new configuration format as input, the script will map the content to the old configuration file format and generate those files into the specified output_dir. + +``` +python3 lifecycle_config.py -o +``` + +# Running Tests + +You may want to use the virtual environment: + +```bash +python3 -m venv myvenv +. myvenv/bin/activate +pip3 install -r requirements.txt +``` + +Execute all tests: + +```bash +pytest tests.py +``` diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py new file mode 100644 index 00000000..4eb849d3 --- /dev/null +++ b/scripts/config_mapping/lifecycle_config.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import argparse +import json +from typing import Dict, Any + +score_defaults = { + "deployment_config": { + "startup_timeout": 0.5, + "shutdown_timeout": 0.5, + "uid": 1000, + "gid" : 1000, + "supplementary_group_ids": [], + "security_policy": "", + "environmental_variables": {}, + "process_arguments": [], + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + "bin_dir": "/opt", + "working_directory": "/tmp", + "restarts_during_startup": 0, + "resource_limits": {} + }, + "component_properties": { + }, + "run_target": { + } +} + + +def load_json_file(file_path: str) -> Dict[str, Any]: + """Load and parse a JSON file.""" + with open(file_path, 'r') as file: + return json.load(file) + + +def preprocess_defaults(config): + """ + This function takes the input configuration and fills in any missing fields with default values. + The resulting file with have no "defaults" entry anymore, but looks like if the user had specified all the fields explicitly. + """ + # TODO + return config + +def gen_health_monitor_config(output_dir, config): + """ + This function generates the health monitor configuration file based on the input configuration. + Input: + output_dir: The directory where the generated files should be saved + config: The preprocessed configuration in the new format, with all defaults applied + + Output: + - A file named "hm_demo.json" containing the health monitor daemon configuration + - A optional file named "hmcore.json" containing the watchdog configuration + - For each supervised process, a file named "_.json" + """ + with open(f"{output_dir}/hm_demo.json", "w") as hm_file: + hm_config = {} + json.dump(hm_config, hm_file, indent=4) + +def gen_launch_manager_config(output_dir, config): + """ + This function generates the launch manager configuration file based on the input configuration. + Input: + output_dir: The directory where the generated files should be saved + config: The preprocessed configuration in the new format, with all defaults applied + + Output: + - A file named "lm_demo.json" containing the launch manager configuration + """ + + with open(f"{output_dir}/lm_demo.json", "w") as lm_file: + lm_config = {} + json.dump(lm_config, lm_file, indent=4) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("filename", help="The launch_manager configuration file") + parser.add_argument("--output-dir", "-o", default=".", help="Output directory for generated files") + args = parser.parse_args() + + input_config = load_json_file(args.filename) + preprocessed_config = preprocess_defaults(input_config) + gen_health_monitor_config(args.output_dir, preprocessed_config) + gen_launch_manager_config(args.output_dir, preprocessed_config) + + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/scripts/config_mapping/requirements.txt b/scripts/config_mapping/requirements.txt new file mode 100644 index 00000000..a7cdacfb --- /dev/null +++ b/scripts/config_mapping/requirements.txt @@ -0,0 +1,2 @@ +pytest>=7.0.0 +pytest-json-report>=1.5.0 diff --git a/scripts/config_mapping/tests.py b/scripts/config_mapping/tests.py new file mode 100644 index 00000000..5dd11fe4 --- /dev/null +++ b/scripts/config_mapping/tests.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +import subprocess +import shutil +from pathlib import Path +import filecmp + +script_dir = Path(__file__).parent +tests_dir = script_dir / "tests" +lifecycle_script = script_dir / "lifecycle_config.py" + +def run(input_file : Path, test_name : str): + """ + Execute the mapping script with the given input file and compare the generated output with the expected output. + Input: + - input_file: The path to the input JSON file for the mapping script + - test_name: The name of the test case, which corresponds to a subdirectory in the "tests" directory containing the expected output + """ + actual_output_dir = tests_dir / test_name / "actual_output" + expected_output_dir = tests_dir / test_name / "expected_output" + + # Clean and create actual output directory + if actual_output_dir.exists(): + shutil.rmtree(actual_output_dir) + actual_output_dir.mkdir(parents=True) + + # Execute lifecycle_config.py + cmd = [ + "python3", + str(lifecycle_script), + str(input_file), + "-o", str(actual_output_dir) + ] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"Command executed successfully: {' '.join(cmd)}") + print(f"Output: {result.stdout}") + except subprocess.CalledProcessError as e: + print(f"Command failed: {' '.join(cmd)}") + print(f"Error: {e.stderr}") + raise + + if not compare_directories(actual_output_dir, expected_output_dir): + raise AssertionError("Actual output does not match expected output.") + +def compare_directories(dir1: Path, dir2: Path) -> bool: + """ + Compare two directories recursively. Return True if they are the same, False otherwise. + """ + dcmp = filecmp.dircmp(dir1, dir2) + + if dcmp.left_only or dcmp.right_only or dcmp.diff_files: + print(f"Directories differ: {dir1} vs {dir2}") + print(f"Only in {dir1}: {dcmp.left_only}") + print(f"Only in {dir2}: {dcmp.right_only}") + print(f"Different files: {dcmp.diff_files}") + return False + + for common_dir in dcmp.common_dirs: + if not compare_directories(dir1 / common_dir, dir2 / common_dir): + return False + + return True + +def test_basic(): + test_name = "basic_test" + input_file = tests_dir / test_name / "input" / "test_input_basic.json" + + run(input_file, test_name="basic_test") + + + + diff --git a/scripts/config_mapping/tests/.gitignore b/scripts/config_mapping/tests/.gitignore new file mode 100644 index 00000000..d1a80c32 --- /dev/null +++ b/scripts/config_mapping/tests/.gitignore @@ -0,0 +1 @@ +*/actual_output \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/input/test_input_basic.json b/scripts/config_mapping/tests/basic_test/input/test_input_basic.json new file mode 100644 index 00000000..3fc7e5f7 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/input/test_input_basic.json @@ -0,0 +1,24 @@ +{ + "processes": [ + { + "name": "test_process", + "executable": "/usr/bin/test_app", + "args": ["--config", "/etc/test.conf"], + "startup_timeout": 1.0, + "shutdown_timeout": 2.0, + "uid": 1001, + "gid": 1001, + "env": { + "LOG_LEVEL": "INFO", + "CONFIG_PATH": "/etc" + } + } + ], + "health_monitors": [ + { + "name": "test_monitor", + "target_process": "test_process", + "check_interval": 5.0 + } + ] +} \ No newline at end of file From f396729d289e2274bfe85a1e48e9f6c1cf3049e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 12 Feb 2026 13:03:46 +0100 Subject: [PATCH 02/72] Add latest example configuration --- .../tests/basic_test/input/lm_config.json | 113 ++++++++++++++++++ .../basic_test/input/test_input_basic.json | 24 ---- 2 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 scripts/config_mapping/tests/basic_test/input/lm_config.json delete mode 100644 scripts/config_mapping/tests/basic_test/input/test_input_basic.json diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json new file mode 100644 index 00000000..039b0647 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -0,0 +1,113 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib", + "GLOBAL_ENV_VAR": "abc", + "EMPTY_GLOBAL_ENV_VAR": "" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [500, 600, 700], + "security_policy": "policy_name", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + "max_memory_usage": 1024, + "max_cpu_ussage": 75 + } + }, + "component_properties": { + "binary_name": "test_app1", + "application_profile": { + "application_type": "REPORTING_AND_SUPERVISED", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": ["test_app2", "test_app3"], + "process_arguments": ["-a", "-b", "--xyz"], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2.0, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["test_app1", "Minimal"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Minimal" + } + } + }, + "Off": { + "description": "Nothing is running" + }, + "initial_run_target": "Minimal", + "final_recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdogs": { + "simple_watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + } +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/input/test_input_basic.json b/scripts/config_mapping/tests/basic_test/input/test_input_basic.json deleted file mode 100644 index 3fc7e5f7..00000000 --- a/scripts/config_mapping/tests/basic_test/input/test_input_basic.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "processes": [ - { - "name": "test_process", - "executable": "/usr/bin/test_app", - "args": ["--config", "/etc/test.conf"], - "startup_timeout": 1.0, - "shutdown_timeout": 2.0, - "uid": 1001, - "gid": 1001, - "env": { - "LOG_LEVEL": "INFO", - "CONFIG_PATH": "/etc" - } - } - ], - "health_monitors": [ - { - "name": "test_monitor", - "target_process": "test_process", - "check_interval": 5.0 - } - ] -} \ No newline at end of file From c49be75c43b65e9d1607b6c238e00b1e906bb9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 12 Feb 2026 15:52:29 +0100 Subject: [PATCH 03/72] Merging of default values --- scripts/config_mapping/Readme.md | 27 ++++++ scripts/config_mapping/lifecycle_config.py | 96 ++++++++++++++++--- .../tests/basic_test/input/lm_config.json | 64 +++++++++++++ 3 files changed, 172 insertions(+), 15 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 5c1ef5d7..6dfc54e8 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -27,3 +27,30 @@ Execute all tests: ```bash pytest tests.py ``` + +# Mapping Details + +## Mapping of RunTargets to ProcessGroups + +TODO + +## Mapping of Components to Processes + +TODO + +### Mapping of Deployment Config to Startup Config + +TODO + +### Mapping of ReadyCondition to Execution Dependencies + +TODO + +## Mapping of Recovery Actions + +## Known Limitations + +TODO + +* Transition timeout +* terminated ready condition diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 4eb849d3..3c049844 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -4,28 +4,57 @@ import json from typing import Dict, Any -score_defaults = { +score_defaults = json.loads(''' +{ "deployment_config": { - "startup_timeout": 0.5, + "ready_timeout": 0.5, "shutdown_timeout": 0.5, - "uid": 1000, - "gid" : 1000, - "supplementary_group_ids": [], - "security_policy": "", "environmental_variables": {}, - "process_arguments": [], - "scheduling_policy": "SCHED_OTHER", - "scheduling_priority": 0, "bin_dir": "/opt", - "working_directory": "/tmp", - "restarts_during_startup": 0, - "resource_limits": {} + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "sandbox": { + "uid": 0, + "gid": 0, + "supplementary_group_ids": [], + "security_policy": "", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } }, "component_properties": { + "application_profile": { + "application_type": "REPORTING", + "is_self_terminating": false + }, + "ready_condition": { + "process_state": "Running" + } }, "run_target": { - } + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": {} } +''') def load_json_file(file_path: str) -> Dict[str, Any]: @@ -39,8 +68,45 @@ def preprocess_defaults(config): This function takes the input configuration and fills in any missing fields with default values. The resulting file with have no "defaults" entry anymore, but looks like if the user had specified all the fields explicitly. """ - # TODO - return config + def dict_merge(dict_a, dict_b): + for key, value in dict_b.items(): + if key in dict_a and isinstance(dict_a[key], dict) and isinstance(value, dict): + dict_a[key] = dict_merge(dict_a[key], value) + elif key not in dict_a: + # Value only exists in dict_b, just add it to dict_a + dict_a[key] = value + else: + # For lists, we want to merge the content + if isinstance(value, list): + dict_a[key].extend(value) + # For primitive types, we want to take the one from dict_b + else: + dict_a[key] = value + return dict_a + + config_defaults = config.get("defaults", {}) + # Starting with score_defaults, then applying the defaults from the config on top. + # This is to ensure that any defaults specified in the input config will override the hardcoded defaults in score_defaults. + merged_defaults = dict_merge(score_defaults, config_defaults) + + new_config = {} + new_config["components"] = {} + components = config.get("components", {}) + for component_name, component_config in components.items(): + new_config["components"][component_name] = {} + new_config["components"][component_name]["description"] = component_config.get("description", "") + # Here we start with the merged defaults, then apply the component config on top, so that any fields specified in the component config will override the defaults. + new_config["components"][component_name]["component_properties"] = dict_merge(merged_defaults["component_properties"], component_config.get("component_properties")) + new_config["components"][component_name]["deployment_config"] = dict_merge(merged_defaults["deployment_config"], component_config.get("deployment_config", {})) + + # Special case: + # If the defaults specify alive_supervision for component, but the component config sets the type to anything other than "SUPERVISED", then we should not apply the + # alive_supervision defaults to that component, since it doesn't make sense to have alive_supervision from the defaults. + # TODO + + print(json.dumps(new_config, indent=4)) + + return new_config def gen_health_monitor_config(output_dir, config): """ diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index 039b0647..7fd3337b 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -69,6 +69,70 @@ "require_magic_close": false } }, + "components": { + "setup_filesystem_sh": { + "description": "Script to mount partitions at the right directories", + "component_properties": { + "binary_name": "bin/setup_filesystem.sh", + "application_profile": { + "application_type": "NATIVE", + "is_self_terminating": true + }, + "process_arguments": ["-a", "-b"], + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts" + } + }, + "dlt-daemon": { + "description": "Logging application", + "component_properties": { + "binary_name": "dltd", + "application_profile": { + "application_type": "NATIVE" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/dlt-daemon" + } + }, + "someip-daemon": { + "description": "SOME/IP application", + "component_properties": { + "binary_name": "someipd" + }, + "deployment_config": { + "bin_dir" : "/opt/apps/someip" + } + }, + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1", + "depends_on": ["dlt-daemon", "someip-daemon"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + }, + "state_manager": { + "description": "Application that manages life cycle of the ECU", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "STATE_MANAGER" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager" + } + } + }, "run_targets": { "Minimal": { "description": "Minimal functionality of the system", From c0bcd0832cb8d88302874f835a61da09e4a13898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 08:39:46 +0100 Subject: [PATCH 04/72] Basic smoketest for defaults preprocessing --- scripts/config_mapping/Readme.md | 3 + scripts/config_mapping/lifecycle_config.py | 37 +++-- scripts/config_mapping/tests.py | 142 +++++++++++++++++- .../tests/basic_test/input/lm_config.json | 8 +- 4 files changed, 173 insertions(+), 17 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 6dfc54e8..ee9e089b 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -53,4 +53,7 @@ TODO TODO * Transition timeout +* CPU time, Memory restrictions * terminated ready condition +* restart attempts - wait Time +* Supported recovery actions diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 3c049844..c79ad8ce 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -52,10 +52,13 @@ "alive_supervision" : { "evaluation_cycle": 0.5 }, - "watchdog": {} + "watchdogs": {} } ''') +# There are various dictionaries in the config where only a single entry is allowed. +# We do not want to merge the defaults with the user specified values for these dictionaries. +not_merging_dicts = ["ready_recovery_action", "recovery_action", "final_recovery_action"] def load_json_file(file_path: str) -> Dict[str, Any]: """Load and parse a JSON file.""" @@ -63,7 +66,7 @@ def load_json_file(file_path: str) -> Dict[str, Any]: return json.load(file) -def preprocess_defaults(config): +def preprocess_defaults(global_defaults, config): """ This function takes the input configuration and fills in any missing fields with default values. The resulting file with have no "defaults" entry anymore, but looks like if the user had specified all the fields explicitly. @@ -71,28 +74,36 @@ def preprocess_defaults(config): def dict_merge(dict_a, dict_b): for key, value in dict_b.items(): if key in dict_a and isinstance(dict_a[key], dict) and isinstance(value, dict): - dict_a[key] = dict_merge(dict_a[key], value) + # For certain dictionaries, we do not want to merge the defaults with the user specified values + if key in not_merging_dicts: + dict_a[key] = value + else: + dict_a[key] = dict_merge(dict_a[key], value) elif key not in dict_a: # Value only exists in dict_b, just add it to dict_a dict_a[key] = value else: - # For lists, we want to merge the content + # For lists, we want to overwrite the content if isinstance(value, list): - dict_a[key].extend(value) + dict_a[key] = (value) # For primitive types, we want to take the one from dict_b else: dict_a[key] = value return dict_a config_defaults = config.get("defaults", {}) - # Starting with score_defaults, then applying the defaults from the config on top. - # This is to ensure that any defaults specified in the input config will override the hardcoded defaults in score_defaults. - merged_defaults = dict_merge(score_defaults, config_defaults) + # Starting with global_defaults, then applying the defaults from the config on top. + # This is to ensure that any defaults specified in the input config will override the hardcoded defaults in global_defaults. + merged_defaults = dict_merge(global_defaults.copy(), config_defaults) + + print("Merged defaults:") + print(json.dumps(merged_defaults, indent=4)) new_config = {} new_config["components"] = {} components = config.get("components", {}) for component_name, component_config in components.items(): + print("Processing component:", component_name) new_config["components"][component_name] = {} new_config["components"][component_name]["description"] = component_config.get("description", "") # Here we start with the merged defaults, then apply the component config on top, so that any fields specified in the component config will override the defaults. @@ -104,6 +115,14 @@ def dict_merge(dict_a, dict_b): # alive_supervision defaults to that component, since it doesn't make sense to have alive_supervision from the defaults. # TODO + new_config["run_targets"] = {} + for run_target, run_target_config in config.get("run_targets", {}).items(): + new_config["run_targets"][run_target] = {} + new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) + + new_config["alive_supervision"] = dict_merge(merged_defaults["alive_supervision"], config.get("alive_supervision", {})) + new_config["watchdogs"] = dict_merge(merged_defaults["watchdogs"], config.get("watchdogs", {})) + print(json.dumps(new_config, indent=4)) return new_config @@ -146,7 +165,7 @@ def main(): args = parser.parse_args() input_config = load_json_file(args.filename) - preprocessed_config = preprocess_defaults(input_config) + preprocessed_config = preprocess_defaults(json.loads(score_defaults), input_config) gen_health_monitor_config(args.output_dir, preprocessed_config) gen_launch_manager_config(args.output_dir, preprocessed_config) diff --git a/scripts/config_mapping/tests.py b/scripts/config_mapping/tests.py index 5dd11fe4..c27c4b5a 100644 --- a/scripts/config_mapping/tests.py +++ b/scripts/config_mapping/tests.py @@ -4,6 +4,8 @@ import shutil from pathlib import Path import filecmp +from lifecycle_config import preprocess_defaults +import json script_dir = Path(__file__).parent tests_dir = script_dir / "tests" @@ -65,10 +67,148 @@ def compare_directories(dir1: Path, dir2: Path) -> bool: def test_basic(): test_name = "basic_test" - input_file = tests_dir / test_name / "input" / "test_input_basic.json" + input_file = tests_dir / test_name / "input" / "lm_config.json" run(input_file, test_name="basic_test") +def test_preprocessing_basic(): + """ + Basic smoketest for the preprocess_defaults function, to ensure that defaults are being applied and overridden correctly. + """ + + global_defaults = json.loads(''' + { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables" : { + "DEFAULT1": "default_value1", + "DEFAULT2": "default_value2" + }, + "sandbox": { + "uid": 0, + "supplementary_group_ids": [100] + } + }, + "component_properties": { + "application_profile": { + "application_type": "REPORTING", + "is_self_terminating": false + } + }, + "alive_supervision": { + "evaluation_cycle": 0.5 + }, + "watchdogs": {} + }''') + + config = json.loads('''{ + "defaults": { + "deployment_config": { + "shutdown_timeout": 1.0, + "environmental_variables" : { + "DEFAULT2": "overridden_value2", + "DEFAULT3": "default_value3", + "DEFAULT4": "default_value4" + }, + "recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + } + }, + "component_properties": { + + } + }, + "components": { + "test_comp": { + "description": "Test component", + "component_properties": { + + }, + "deployment_config": { + "environmental_variables": { + "DEFAULT3": "overridden_value3" + }, + "sandbox": { + "uid": 0, + "gid": 1, + "supplementary_group_ids": [101] + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + } + } + }, + "run_targets": {}, + "alive_supervision": { + "evaluation_cycle": 0.1 + }, + "watchdogs": { + "simple_watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + } + }''') + + preprocessed_config = preprocess_defaults(global_defaults, config) + + expected_config=json.loads('''{ + "components": { + "test_comp": { + "description": "Test component", + "component_properties": { + "application_profile": { + "application_type": "REPORTING", + "is_self_terminating": false + } + }, + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 1.0, + "environmental_variables" : { + "DEFAULT1": "default_value1", + "DEFAULT2": "overridden_value2", + "DEFAULT3": "overridden_value3", + "DEFAULT4": "default_value4" + }, + "sandbox": { + "uid": 0, + "gid":1, + "supplementary_group_ids": [101] + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + } + } + }, + "run_targets": {}, + "alive_supervision": { + "evaluation_cycle": 0.1 + }, + "watchdogs": { + "simple_watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + } + }''') + print("Dumping preprocessed configuration:") + print(json.dumps(preprocessed_config, indent=4)) + assert preprocessed_config == expected_config, "Preprocessed config does not match expected config." diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index 7fd3337b..3e2b8ebb 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -30,7 +30,7 @@ "scheduling_policy": "SCHED_OTHER", "scheduling_priority": 0, "max_memory_usage": 1024, - "max_cpu_ussage": 75 + "max_cpu_usage": 75 } }, "component_properties": { @@ -61,12 +61,6 @@ }, "alive_supervision" : { "evaluation_cycle": 0.5 - }, - "watchdog": { - "device_file_path": "/dev/watchdog", - "max_timeout": 2.0, - "deactivate_on_shutdown": true, - "require_magic_close": false } }, "components": { From 19776f9602c620df2cecf0cdc65d29e855f1a0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 11:26:32 +0100 Subject: [PATCH 05/72] Start to map PHM configuration --- scripts/config_mapping/Readme.md | 7 ++ scripts/config_mapping/lifecycle_config.py | 99 +++++++++++++++++++--- scripts/config_mapping/tests.py | 30 ++++++- 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index ee9e089b..9c5cc501 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -48,10 +48,17 @@ TODO ## Mapping of Recovery Actions +## Mapping of Alive Supervision + +## Mapping of Watchdog Configuration + ## Known Limitations TODO +* What if an object is explicitly set to {} in the config? Will this overwrite the default to None? +* What about supervision and state manager? + * Transition timeout * CPU time, Memory restrictions * terminated ready condition diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index c79ad8ce..2c3232bd 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +from copy import deepcopy import json from typing import Dict, Any @@ -65,7 +66,6 @@ def load_json_file(file_path: str) -> Dict[str, Any]: with open(file_path, 'r') as file: return json.load(file) - def preprocess_defaults(global_defaults, config): """ This function takes the input configuration and fills in any missing fields with default values. @@ -94,21 +94,18 @@ def dict_merge(dict_a, dict_b): config_defaults = config.get("defaults", {}) # Starting with global_defaults, then applying the defaults from the config on top. # This is to ensure that any defaults specified in the input config will override the hardcoded defaults in global_defaults. - merged_defaults = dict_merge(global_defaults.copy(), config_defaults) - - print("Merged defaults:") - print(json.dumps(merged_defaults, indent=4)) + merged_defaults = dict_merge(deepcopy(global_defaults), config_defaults) new_config = {} new_config["components"] = {} components = config.get("components", {}) for component_name, component_config in components.items(): - print("Processing component:", component_name) + #print("Processing component:", component_name) new_config["components"][component_name] = {} new_config["components"][component_name]["description"] = component_config.get("description", "") # Here we start with the merged defaults, then apply the component config on top, so that any fields specified in the component config will override the defaults. - new_config["components"][component_name]["component_properties"] = dict_merge(merged_defaults["component_properties"], component_config.get("component_properties")) - new_config["components"][component_name]["deployment_config"] = dict_merge(merged_defaults["deployment_config"], component_config.get("deployment_config", {})) + new_config["components"][component_name]["component_properties"] = dict_merge(deepcopy(merged_defaults["component_properties"]), component_config.get("component_properties")) + new_config["components"][component_name]["deployment_config"] = dict_merge(deepcopy(merged_defaults["deployment_config"]), component_config.get("deployment_config", {})) # Special case: # If the defaults specify alive_supervision for component, but the component config sets the type to anything other than "SUPERVISED", then we should not apply the @@ -117,13 +114,16 @@ def dict_merge(dict_a, dict_b): new_config["run_targets"] = {} for run_target, run_target_config in config.get("run_targets", {}).items(): - new_config["run_targets"][run_target] = {} - new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) + # TODO: initial_run_target is not a dictionary, merging defautls not working for this currently + if run_target == "initial_run_target": + new_config["run_targets"][run_target] = run_target_config + else: + new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) new_config["alive_supervision"] = dict_merge(merged_defaults["alive_supervision"], config.get("alive_supervision", {})) new_config["watchdogs"] = dict_merge(merged_defaults["watchdogs"], config.get("watchdogs", {})) - print(json.dumps(new_config, indent=4)) + #print(json.dumps(new_config, indent=4)) return new_config @@ -139,8 +139,81 @@ def gen_health_monitor_config(output_dir, config): - A optional file named "hmcore.json" containing the watchdog configuration - For each supervised process, a file named "_.json" """ + def get_process_type(application_type): + if application_type == "STATE_MANAGER": + return "STATE_MANAGEMENT" + else: + return "REGULAR_PROCESS" + + def is_supervised(application_type): + return application_type == "STATE_MANAGER" or application_type == "REPORTING_AND_SUPERVISED" + + hm_config = {} + hm_config["versionMajor"] = 8 + hm_config["versionMinor"] = 0 + hm_config["process"]= [] + hm_config["hmMonitorInterface"] = [] + hm_config["hmSupervisionCheckpoint"] = [] + hm_config["hmAliveSupervision"] = [] + hm_config["hmDeadlineSupervision"] = [] + hm_config["hmLogicalSupervision"] = [] + hm_config["hmLocalSupervision"] = [] + index = 0 + for component_name, component_config in config["components"].items(): + if is_supervised(component_config["component_properties"]["application_profile"]["application_type"]): + process = {} + process["index"] = index + process["shortName"] = component_name + process["identifier"] = component_name + process["processType"] = get_process_type(component_config["component_properties"]["application_profile"]["application_type"]) + process["refProcessGroupStates"] = [] # TODO, Need to know all RunTargets where this process runs + process["processExecutionErrors"] = {"processExecutionError":1} + hm_config["process"].append(process) + + hmMonitorIf = {} + hmMonitorIf["instanceSpecifier"] = component_name + hmMonitorIf["processShortName"] = component_name + hmMonitorIf["portPrototype"] = "DefaultPort" + hmMonitorIf["interfacePath"] = "lifecycle_health" + component_name + hmMonitorIf["refProcessIndex"] = index + hmMonitorIf["permittedUid"] = component_config["deployment_config"]["sandbox"]["uid"] + hm_config["hmMonitorInterface"].append(hmMonitorIf) + + checkpoint = {} + checkpoint["shortName"] = component_name + "_checkpoint" + checkpoint["checkpointId"] = 1 + checkpoint["refInterfaceIndex"] = index + hm_config["hmSupervisionCheckpoint"].append(checkpoint) + + alive_supervision = {} + alive_supervision["ruleContextKey"] = component_name + "_alive_supervision" + alive_supervision["refCheckPointIndex"] = index + alive_supervision["aliveReferenceCycle"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["reporting_cycle"] + alive_supervision["minAliveIndications"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["min_indications"] + alive_supervision["maxAliveIndications"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["max_indications"] + alive_supervision["isMinCheckDisabled"] = alive_supervision["minAliveIndications"] == 0 + alive_supervision["isMaxCheckDisabled"] = alive_supervision["maxAliveIndications"] == 0 + alive_supervision["failedSupervisionCyclesTolerance"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["failed_cycles_tolerance"] + alive_supervision["refProcessIndex"] = index + alive_supervision["refProcessGroupStates"] = [] # TODO, Need to know all RunTargets where this process runs + hm_config["hmAliveSupervision"].append(alive_supervision) + + local_supervision = {} + local_supervision["ruleContextKey"] = component_name + "_local_supervision" + local_supervision["infoRefInterfacePath"] = "" + local_supervision["hmRefAliveSupervision"] = [] + local_supervision["hmRefAliveSupervision"].append({"refAliveSupervisionIdx": index}) + hm_config["hmLocalSupervision"].append(local_supervision) + + #with open(f"{output_dir}/{process_name}_{process_group}.json", "w") as process_file: + # json.dump(process_config, process_file, indent=4) + + index += 1 + + # TODO: Add global supervision + # TODO: Add RecoveryAction + with open(f"{output_dir}/hm_demo.json", "w") as hm_file: - hm_config = {} json.dump(hm_config, hm_file, indent=4) def gen_launch_manager_config(output_dir, config): @@ -165,7 +238,7 @@ def main(): args = parser.parse_args() input_config = load_json_file(args.filename) - preprocessed_config = preprocess_defaults(json.loads(score_defaults), input_config) + preprocessed_config = preprocess_defaults(score_defaults, input_config) gen_health_monitor_config(args.output_dir, preprocessed_config) gen_launch_manager_config(args.output_dir, preprocessed_config) diff --git a/scripts/config_mapping/tests.py b/scripts/config_mapping/tests.py index c27c4b5a..58075aa0 100644 --- a/scripts/config_mapping/tests.py +++ b/scripts/config_mapping/tests.py @@ -11,7 +11,7 @@ tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" -def run(input_file : Path, test_name : str): +def run(input_file : Path, test_name : str, compare_files_only=[]): """ Execute the mapping script with the given input file and compare the generated output with the expected output. Input: @@ -43,8 +43,14 @@ def run(input_file : Path, test_name : str): print(f"Error: {e.stderr}") raise - if not compare_directories(actual_output_dir, expected_output_dir): - raise AssertionError("Actual output does not match expected output.") + if compare_files_only: + # Compare only specific files + if not compare_files(actual_output_dir, expected_output_dir, compare_files_only): + raise AssertionError("Actual output files do not match expected output files.") + else: + # Compare the complete directory content + if not compare_directories(actual_output_dir, expected_output_dir): + raise AssertionError("Actual output does not match expected output.") def compare_directories(dir1: Path, dir2: Path) -> bool: """ @@ -65,12 +71,30 @@ def compare_directories(dir1: Path, dir2: Path) -> bool: return True +def compare_files(dir1 : Path, dir2 : Path, files : list) -> bool: + """ + Compare specific files in two directories. Return True if they are the same, False otherwise. + """ + for file in files: + file1 = dir1 / file + file2 = dir2 / file + if not filecmp.cmp(file1, file2, shallow=False): + print(f"Files differ: {file1} vs {file2}") + return False + return True + def test_basic(): test_name = "basic_test" input_file = tests_dir / test_name / "input" / "lm_config.json" run(input_file, test_name="basic_test") +def test_health_config_mapping(): + #test_name = "health_config_test" + #input_file = tests_dir / test_name / "input" / "lm_config.json" + # + #run(input_file, test_name="basic_test", compare_files_only=["hm_demo.json"]) + pass def test_preprocessing_basic(): """ From 646f0c2b53357d9b1ca8bd23348eb6c45ed5eb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 13:27:53 +0100 Subject: [PATCH 06/72] Separate unit and integration tests --- scripts/config_mapping/integration_tests.py | 97 +++++++++++++++++++ .../{tests.py => unit_tests.py} | 93 ------------------ 2 files changed, 97 insertions(+), 93 deletions(-) create mode 100644 scripts/config_mapping/integration_tests.py rename scripts/config_mapping/{tests.py => unit_tests.py} (57%) diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py new file mode 100644 index 00000000..4fc1e644 --- /dev/null +++ b/scripts/config_mapping/integration_tests.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +import subprocess +import shutil +from pathlib import Path +import filecmp +from lifecycle_config import preprocess_defaults +import json + +script_dir = Path(__file__).parent +tests_dir = script_dir / "tests" +lifecycle_script = script_dir / "lifecycle_config.py" + +def run(input_file : Path, test_name : str, compare_files_only=[]): + """ + Execute the mapping script with the given input file and compare the generated output with the expected output. + Input: + - input_file: The path to the input JSON file for the mapping script + - test_name: The name of the test case, which corresponds to a subdirectory in the "tests" directory containing the expected output + """ + actual_output_dir = tests_dir / test_name / "actual_output" + expected_output_dir = tests_dir / test_name / "expected_output" + + # Clean and create actual output directory + if actual_output_dir.exists(): + shutil.rmtree(actual_output_dir) + actual_output_dir.mkdir(parents=True) + + # Execute lifecycle_config.py + cmd = [ + "python3", + str(lifecycle_script), + str(input_file), + "-o", str(actual_output_dir) + ] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"Command executed successfully: {' '.join(cmd)}") + print(f"Output: {result.stdout}") + except subprocess.CalledProcessError as e: + print(f"Command failed: {' '.join(cmd)}") + print(f"Error: {e.stderr}") + raise + + if compare_files_only: + # Compare only specific files + if not compare_files(actual_output_dir, expected_output_dir, compare_files_only): + raise AssertionError("Actual output files do not match expected output files.") + else: + # Compare the complete directory content + if not compare_directories(actual_output_dir, expected_output_dir): + raise AssertionError("Actual output does not match expected output.") + +def compare_directories(dir1: Path, dir2: Path) -> bool: + """ + Compare two directories recursively. Return True if they are the same, False otherwise. + """ + dcmp = filecmp.dircmp(dir1, dir2) + + if dcmp.left_only or dcmp.right_only or dcmp.diff_files: + print(f"Directories differ: {dir1} vs {dir2}") + print(f"Only in {dir1}: {dcmp.left_only}") + print(f"Only in {dir2}: {dcmp.right_only}") + print(f"Different files: {dcmp.diff_files}") + return False + + for common_dir in dcmp.common_dirs: + if not compare_directories(dir1 / common_dir, dir2 / common_dir): + return False + + return True + +def compare_files(dir1 : Path, dir2 : Path, files : list) -> bool: + """ + Compare specific files in two directories. Return True if they are the same, False otherwise. + """ + for file in files: + file1 = dir1 / file + file2 = dir2 / file + if not filecmp.cmp(file1, file2, shallow=False): + print(f"Files differ: {file1} vs {file2}") + return False + return True + +def test_basic(): + test_name = "basic_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name="basic_test") + +#def test_health_config_mapping(): +# test_name = "health_config_test" +# input_file = tests_dir / test_name / "input" / "lm_config.json" +# +# run(input_file, test_name="basic_test", compare_files_only=["hm_demo.json"]) +# pass diff --git a/scripts/config_mapping/tests.py b/scripts/config_mapping/unit_tests.py similarity index 57% rename from scripts/config_mapping/tests.py rename to scripts/config_mapping/unit_tests.py index 58075aa0..12dc6131 100644 --- a/scripts/config_mapping/tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -1,101 +1,8 @@ #!/usr/bin/env python3 -import subprocess -import shutil -from pathlib import Path -import filecmp from lifecycle_config import preprocess_defaults import json -script_dir = Path(__file__).parent -tests_dir = script_dir / "tests" -lifecycle_script = script_dir / "lifecycle_config.py" - -def run(input_file : Path, test_name : str, compare_files_only=[]): - """ - Execute the mapping script with the given input file and compare the generated output with the expected output. - Input: - - input_file: The path to the input JSON file for the mapping script - - test_name: The name of the test case, which corresponds to a subdirectory in the "tests" directory containing the expected output - """ - actual_output_dir = tests_dir / test_name / "actual_output" - expected_output_dir = tests_dir / test_name / "expected_output" - - # Clean and create actual output directory - if actual_output_dir.exists(): - shutil.rmtree(actual_output_dir) - actual_output_dir.mkdir(parents=True) - - # Execute lifecycle_config.py - cmd = [ - "python3", - str(lifecycle_script), - str(input_file), - "-o", str(actual_output_dir) - ] - - try: - result = subprocess.run(cmd, check=True, capture_output=True, text=True) - print(f"Command executed successfully: {' '.join(cmd)}") - print(f"Output: {result.stdout}") - except subprocess.CalledProcessError as e: - print(f"Command failed: {' '.join(cmd)}") - print(f"Error: {e.stderr}") - raise - - if compare_files_only: - # Compare only specific files - if not compare_files(actual_output_dir, expected_output_dir, compare_files_only): - raise AssertionError("Actual output files do not match expected output files.") - else: - # Compare the complete directory content - if not compare_directories(actual_output_dir, expected_output_dir): - raise AssertionError("Actual output does not match expected output.") - -def compare_directories(dir1: Path, dir2: Path) -> bool: - """ - Compare two directories recursively. Return True if they are the same, False otherwise. - """ - dcmp = filecmp.dircmp(dir1, dir2) - - if dcmp.left_only or dcmp.right_only or dcmp.diff_files: - print(f"Directories differ: {dir1} vs {dir2}") - print(f"Only in {dir1}: {dcmp.left_only}") - print(f"Only in {dir2}: {dcmp.right_only}") - print(f"Different files: {dcmp.diff_files}") - return False - - for common_dir in dcmp.common_dirs: - if not compare_directories(dir1 / common_dir, dir2 / common_dir): - return False - - return True - -def compare_files(dir1 : Path, dir2 : Path, files : list) -> bool: - """ - Compare specific files in two directories. Return True if they are the same, False otherwise. - """ - for file in files: - file1 = dir1 / file - file2 = dir2 / file - if not filecmp.cmp(file1, file2, shallow=False): - print(f"Files differ: {file1} vs {file2}") - return False - return True - -def test_basic(): - test_name = "basic_test" - input_file = tests_dir / test_name / "input" / "lm_config.json" - - run(input_file, test_name="basic_test") - -def test_health_config_mapping(): - #test_name = "health_config_test" - #input_file = tests_dir / test_name / "input" / "lm_config.json" - # - #run(input_file, test_name="basic_test", compare_files_only=["hm_demo.json"]) - pass - def test_preprocessing_basic(): """ Basic smoketest for the preprocess_defaults function, to ensure that defaults are being applied and overridden correctly. From 5d2c385662422b2960ab9e252292d3a4e3df7ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 13:52:40 +0100 Subject: [PATCH 07/72] Document mapping decisions and initial known limitations --- scripts/config_mapping/Readme.md | 44 +++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 9c5cc501..1a8d9aa3 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -22,45 +22,65 @@ python3 -m venv myvenv pip3 install -r requirements.txt ``` -Execute all tests: +Execute tests: ```bash -pytest tests.py +pytest unit_tests.py +pytest integration_tests.py ``` # Mapping Details ## Mapping of RunTargets to ProcessGroups -TODO +The LaunchManager will be configured with only a single Process Group called `MainPG`. +Each RunTarget will be mapped to a ProcessGroupState with the same name. +For example, RunTarget `Minimal` will result in a ProcessGroupState called `MainPG/Minimal`. +The ProcessGroupState will contain all the processes that would be started as part of the associated RunTarget. + +The LaunchManager will startup up `MainPG/Startup` by default. Therefore, we require for now that `initial_run_target` must be set to `Startup`. ## Mapping of Components to Processes -TODO +There is a 1:1 mapping from Component to Processes. ### Mapping of Deployment Config to Startup Config -TODO +There is a 1:1 mapping from deployment config to startup config. +Every Component can only have a single deployment config, therefore the mapped Process configuration will only have a single startup config. ### Mapping of ReadyCondition to Execution Dependencies -TODO +The ReadyCondition of a Component is mapped to an execution dependency between two processes. +If Component A has ReadyCondition `process_state:Running` and Component B depends on Component A. Then the ReadyCondition of Component A is mapped to `Component B depends on Component A in State Running`. + +For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that are not directly assigned to a RunTarget. Otherwise, this ReadyCondition cannot be mapped to an execution dependency. ## Mapping of Recovery Actions +The only supported RecoveryAction during startup of a Component is the restart of a Component. This RecoveryAction is mapped to the `restartAttempts` parameter in the old configuration. + +The RecoveryAction after component startup (parameter `components//deployment_config/recovery_action`) as well as the RecoveryAction for RunTargets (parameter `run_targets//recovery_action`) are currently not supported. + +The `run_targets/final_recovery_action` RecoveryAction will be mapped to the `ModeGroup/recoveryMode_name` parameter. This will initiate a transition to the target ProcessGroupState/RunTarget when a process crashes at runtime or a supervision fails. We assume that this transition must not fail. + + ## Mapping of Alive Supervision ## Mapping of Watchdog Configuration ## Known Limitations -TODO +* The sandbox parameters `max_memory_usage` and `max_cpu_usage` are currently not supported. +* The initial RunTarget must be named `Startup` and the `initial_run_target` must be configured to `Startup`. +* For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that are not directly assigned to a RunTarget +* The `ready_recovery_action` only supports the RecoveryAction of type `restart` +* The parameters `components//deployment_config/recovery_action` and `run_targets//recovery_action` are currently not supported. Only the global `final_recovery_action` is supported +* The parameter `run_targets//transition_timeout` is currently not supported + + +Open topics: * What if an object is explicitly set to {} in the config? Will this overwrite the default to None? * What about supervision and state manager? -* Transition timeout -* CPU time, Memory restrictions -* terminated ready condition -* restart attempts - wait Time -* Supported recovery actions From c4afbb9878aeaa1c1212fa5e69932123bd160c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 13:57:28 +0100 Subject: [PATCH 08/72] Copy dict only internally when merging defaults --- scripts/config_mapping/lifecycle_config.py | 41 ++++++++++++---------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 2c3232bd..219b02c1 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -72,29 +72,32 @@ def preprocess_defaults(global_defaults, config): The resulting file with have no "defaults" entry anymore, but looks like if the user had specified all the fields explicitly. """ def dict_merge(dict_a, dict_b): - for key, value in dict_b.items(): - if key in dict_a and isinstance(dict_a[key], dict) and isinstance(value, dict): - # For certain dictionaries, we do not want to merge the defaults with the user specified values - if key in not_merging_dicts: + def dict_merge_recursive(dict_a, dict_b): + for key, value in dict_b.items(): + if key in dict_a and isinstance(dict_a[key], dict) and isinstance(value, dict): + # For certain dictionaries, we do not want to merge the defaults with the user specified values + if key in not_merging_dicts: + dict_a[key] = value + else: + dict_a[key] = dict_merge(dict_a[key], value) + elif key not in dict_a: + # Value only exists in dict_b, just add it to dict_a dict_a[key] = value else: - dict_a[key] = dict_merge(dict_a[key], value) - elif key not in dict_a: - # Value only exists in dict_b, just add it to dict_a - dict_a[key] = value - else: - # For lists, we want to overwrite the content - if isinstance(value, list): - dict_a[key] = (value) - # For primitive types, we want to take the one from dict_b - else: - dict_a[key] = value - return dict_a + # For lists, we want to overwrite the content + if isinstance(value, list): + dict_a[key] = (value) + # For primitive types, we want to take the one from dict_b + else: + dict_a[key] = value + return dict_a + # We are changing the content of dict_a, so we need a deep copy + return dict_merge_recursive(deepcopy(dict_a), dict_b) config_defaults = config.get("defaults", {}) # Starting with global_defaults, then applying the defaults from the config on top. # This is to ensure that any defaults specified in the input config will override the hardcoded defaults in global_defaults. - merged_defaults = dict_merge(deepcopy(global_defaults), config_defaults) + merged_defaults = dict_merge(global_defaults, config_defaults) new_config = {} new_config["components"] = {} @@ -104,8 +107,8 @@ def dict_merge(dict_a, dict_b): new_config["components"][component_name] = {} new_config["components"][component_name]["description"] = component_config.get("description", "") # Here we start with the merged defaults, then apply the component config on top, so that any fields specified in the component config will override the defaults. - new_config["components"][component_name]["component_properties"] = dict_merge(deepcopy(merged_defaults["component_properties"]), component_config.get("component_properties")) - new_config["components"][component_name]["deployment_config"] = dict_merge(deepcopy(merged_defaults["deployment_config"]), component_config.get("deployment_config", {})) + new_config["components"][component_name]["component_properties"] = dict_merge(merged_defaults["component_properties"], component_config.get("component_properties")) + new_config["components"][component_name]["deployment_config"] = dict_merge(merged_defaults["deployment_config"], component_config.get("deployment_config", {})) # Special case: # If the defaults specify alive_supervision for component, but the component config sets the type to anything other than "SUPERVISED", then we should not apply the From 160a6247e0493455b63b55490400b3a0ff660c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 14:54:22 +0100 Subject: [PATCH 09/72] Initial mapping of hm configuration --- scripts/config_mapping/Readme.md | 6 +- scripts/config_mapping/lifecycle_config.py | 91 +++++++++++++++++++--- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 1a8d9aa3..94e7c7e9 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -64,9 +64,13 @@ The RecoveryAction after component startup (parameter `components//de The `run_targets/final_recovery_action` RecoveryAction will be mapped to the `ModeGroup/recoveryMode_name` parameter. This will initiate a transition to the target ProcessGroupState/RunTarget when a process crashes at runtime or a supervision fails. We assume that this transition must not fail. - ## Mapping of Alive Supervision +For each Component with application_type `REPORTING_AND_SUPERVISED` or `STATE_MANAGER`, we will create an Alive Supervision configuration in the old configuration format. There is a 1:1 mapping from the `component_properties/application_profile/alive_supervision` parameters to the old configuration Alive Supervision structure. + +Furthermore, all the Alive Supervisions for non-StateManager processes (application type `REPORTING_AND_SUPERVISED`) are bundled in a GlobalSupervision. Failure of any of those supervisions will trigger the transition to the `final_recovery_action`. +The Alive Supervision for StateManager processes (application type `STATE_MANAGER`) are bundled in a separate supervision. Failures for any of those supervisions will lead to watchdog reset. + ## Mapping of Watchdog Configuration ## Known Limitations diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 219b02c1..5b5b7d86 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -151,9 +151,31 @@ def get_process_type(application_type): def is_supervised(application_type): return application_type == "STATE_MANAGER" or application_type == "REPORTING_AND_SUPERVISED" + def is_state_manager(application_type): + return application_type == "STATE_MANAGER" + + def get_all_process_group_states(run_targets): + process_group_states = [] + for run_target, _ in run_targets.items(): + if run_target not in ["initial_run_target", "final_recovery_action"]: + process_group_states.append("MainPG/"+run_target) + return process_group_states + + def get_all_refProcessGroupStates(run_targets): + states = get_all_process_group_states(run_targets) + refProcessGroupStates = [] + for state in states: + refProcessGroupStates.append({"identifier": state}) + return refProcessGroupStates + + def get_recovery_process_group_state(config): + return "MainPG/" + config.get("run_targets", {}).get("final_recovery_action", {}).get("switch_run_target", {}).get("run_target", "Off") + + SCHEMA_VERSION_MAJOR = 8 + SCHEMA_VERSION_MINOR = 0 hm_config = {} - hm_config["versionMajor"] = 8 - hm_config["versionMinor"] = 0 + hm_config["versionMajor"] = SCHEMA_VERSION_MAJOR + hm_config["versionMinor"] = SCHEMA_VERSION_MINOR hm_config["process"]= [] hm_config["hmMonitorInterface"] = [] hm_config["hmSupervisionCheckpoint"] = [] @@ -161,15 +183,22 @@ def is_supervised(application_type): hm_config["hmDeadlineSupervision"] = [] hm_config["hmLogicalSupervision"] = [] hm_config["hmLocalSupervision"] = [] + hm_config["hmGlobalSupervision"] = [] + hm_config["hmRecoveryNotification"] = [] index = 0 + state_manager_indices = [] for component_name, component_config in config["components"].items(): if is_supervised(component_config["component_properties"]["application_profile"]["application_type"]): + # Keep track of state managers, as any supervision failure here should fire the watchdog + if is_state_manager(component_config["component_properties"]["application_profile"]["application_type"]): + state_manager_indices.append(index) + process = {} process["index"] = index process["shortName"] = component_name process["identifier"] = component_name process["processType"] = get_process_type(component_config["component_properties"]["application_profile"]["application_type"]) - process["refProcessGroupStates"] = [] # TODO, Need to know all RunTargets where this process runs + process["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) process["processExecutionErrors"] = {"processExecutionError":1} hm_config["process"].append(process) @@ -198,23 +227,65 @@ def is_supervised(application_type): alive_supervision["isMaxCheckDisabled"] = alive_supervision["maxAliveIndications"] == 0 alive_supervision["failedSupervisionCyclesTolerance"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["failed_cycles_tolerance"] alive_supervision["refProcessIndex"] = index - alive_supervision["refProcessGroupStates"] = [] # TODO, Need to know all RunTargets where this process runs + alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) hm_config["hmAliveSupervision"].append(alive_supervision) local_supervision = {} local_supervision["ruleContextKey"] = component_name + "_local_supervision" local_supervision["infoRefInterfacePath"] = "" - local_supervision["hmRefAliveSupervision"] = [] - local_supervision["hmRefAliveSupervision"].append({"refAliveSupervisionIdx": index}) + local_supervision["hmRefAliveSupervision"] = [{"refAliveSupervisionIdx": index}] hm_config["hmLocalSupervision"].append(local_supervision) - #with open(f"{output_dir}/{process_name}_{process_group}.json", "w") as process_file: - # json.dump(process_config, process_file, indent=4) + with open(f"{output_dir}/{component_name}.json", "w") as process_file: + process_config = {} + process_config["versionMajor"] = SCHEMA_VERSION_MAJOR + process_config["versionMinor"] = SCHEMA_VERSION_MINOR + process_config["process"] = [] + process_config["hmMonitorInterface"] = [] + process_config["hmMonitorInterface"].append(hmMonitorIf) + json.dump(process_config, process_file, indent=4) index += 1 - # TODO: Add global supervision - # TODO: Add RecoveryAction + if len(state_manager_indices) > 0: + # Create a global supervision & recovery action for StateManager processes. + # If this supervision fails it leads to a recovery action with property "shouldFireWatchdog:true" + state_manager_global_supervision = {} + state_manager_global_supervision["ruleContextKey"] = "StateManager_global_supervision" + state_manager_global_supervision["isSeverityCritical"] = True + state_manager_global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in state_manager_indices] + state_manager_global_supervision["refProcesses"] = [{"index": idx} for idx in state_manager_indices] + state_manager_global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + hm_config["hmGlobalSupervision"].append(state_manager_global_supervision) + + state_manager_recovery_action = {} + state_manager_recovery_action["ruleContextKey"] = "StateManager_recovery_notification" + state_manager_recovery_action["recoveryNotificationTimeout"] = 5000 + state_manager_recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) + state_manager_recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(state_manager_global_supervision) + state_manager_recovery_action["instanceSpecifier"] = "" + state_manager_recovery_action["shouldFireWatchdog"] = True + hm_config["hmRecoveryNotification"].append(state_manager_recovery_action) + + non_state_manager_indices = [i for i in range(index) if i not in state_manager_indices] + if len(non_state_manager_indices) > 0: + # Create a global supervision & recovery action for non-StateManager processes. + non_state_manager_global_supervision = {} + non_state_manager_global_supervision["ruleContextKey"] = "global_supervision" + non_state_manager_global_supervision["isSeverityCritical"] = False + non_state_manager_global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in non_state_manager_indices] + non_state_manager_global_supervision["refProcesses"] = [{"index": idx} for idx in non_state_manager_indices] + non_state_manager_global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + hm_config["hmGlobalSupervision"].append(non_state_manager_global_supervision) + + non_state_manager_recovery_action = {} + non_state_manager_recovery_action["ruleContextKey"] = "recovery_notification" + non_state_manager_recovery_action["recoveryNotificationTimeout"] = 5000 + non_state_manager_recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) + non_state_manager_recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(non_state_manager_global_supervision) + non_state_manager_recovery_action["instanceSpecifier"] = "" + non_state_manager_recovery_action["shouldFireWatchdog"] = False + hm_config["hmRecoveryNotification"].append(non_state_manager_recovery_action) with open(f"{output_dir}/hm_demo.json", "w") as hm_file: json.dump(hm_config, hm_file, indent=4) From c0686fcdf30378291c833bbddfac99d22e955155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 14:57:18 +0100 Subject: [PATCH 10/72] Treat StateManager supervision same as other supervisions --- scripts/config_mapping/Readme.md | 3 - scripts/config_mapping/lifecycle_config.py | 66 +++++++--------------- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 94e7c7e9..0b6185e7 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -68,9 +68,6 @@ The `run_targets/final_recovery_action` RecoveryAction will be mapped to the `Mo For each Component with application_type `REPORTING_AND_SUPERVISED` or `STATE_MANAGER`, we will create an Alive Supervision configuration in the old configuration format. There is a 1:1 mapping from the `component_properties/application_profile/alive_supervision` parameters to the old configuration Alive Supervision structure. -Furthermore, all the Alive Supervisions for non-StateManager processes (application type `REPORTING_AND_SUPERVISED`) are bundled in a GlobalSupervision. Failure of any of those supervisions will trigger the transition to the `final_recovery_action`. -The Alive Supervision for StateManager processes (application type `STATE_MANAGER`) are bundled in a separate supervision. Failures for any of those supervisions will lead to watchdog reset. - ## Mapping of Watchdog Configuration ## Known Limitations diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 5b5b7d86..73bc29e2 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -151,9 +151,6 @@ def get_process_type(application_type): def is_supervised(application_type): return application_type == "STATE_MANAGER" or application_type == "REPORTING_AND_SUPERVISED" - def is_state_manager(application_type): - return application_type == "STATE_MANAGER" - def get_all_process_group_states(run_targets): process_group_states = [] for run_target, _ in run_targets.items(): @@ -186,13 +183,8 @@ def get_recovery_process_group_state(config): hm_config["hmGlobalSupervision"] = [] hm_config["hmRecoveryNotification"] = [] index = 0 - state_manager_indices = [] for component_name, component_config in config["components"].items(): if is_supervised(component_config["component_properties"]["application_profile"]["application_type"]): - # Keep track of state managers, as any supervision failure here should fire the watchdog - if is_state_manager(component_config["component_properties"]["application_profile"]["application_type"]): - state_manager_indices.append(index) - process = {} process["index"] = index process["shortName"] = component_name @@ -247,45 +239,25 @@ def get_recovery_process_group_state(config): index += 1 - if len(state_manager_indices) > 0: - # Create a global supervision & recovery action for StateManager processes. - # If this supervision fails it leads to a recovery action with property "shouldFireWatchdog:true" - state_manager_global_supervision = {} - state_manager_global_supervision["ruleContextKey"] = "StateManager_global_supervision" - state_manager_global_supervision["isSeverityCritical"] = True - state_manager_global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in state_manager_indices] - state_manager_global_supervision["refProcesses"] = [{"index": idx} for idx in state_manager_indices] - state_manager_global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) - hm_config["hmGlobalSupervision"].append(state_manager_global_supervision) - - state_manager_recovery_action = {} - state_manager_recovery_action["ruleContextKey"] = "StateManager_recovery_notification" - state_manager_recovery_action["recoveryNotificationTimeout"] = 5000 - state_manager_recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) - state_manager_recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(state_manager_global_supervision) - state_manager_recovery_action["instanceSpecifier"] = "" - state_manager_recovery_action["shouldFireWatchdog"] = True - hm_config["hmRecoveryNotification"].append(state_manager_recovery_action) - - non_state_manager_indices = [i for i in range(index) if i not in state_manager_indices] - if len(non_state_manager_indices) > 0: - # Create a global supervision & recovery action for non-StateManager processes. - non_state_manager_global_supervision = {} - non_state_manager_global_supervision["ruleContextKey"] = "global_supervision" - non_state_manager_global_supervision["isSeverityCritical"] = False - non_state_manager_global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in non_state_manager_indices] - non_state_manager_global_supervision["refProcesses"] = [{"index": idx} for idx in non_state_manager_indices] - non_state_manager_global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) - hm_config["hmGlobalSupervision"].append(non_state_manager_global_supervision) - - non_state_manager_recovery_action = {} - non_state_manager_recovery_action["ruleContextKey"] = "recovery_notification" - non_state_manager_recovery_action["recoveryNotificationTimeout"] = 5000 - non_state_manager_recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) - non_state_manager_recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(non_state_manager_global_supervision) - non_state_manager_recovery_action["instanceSpecifier"] = "" - non_state_manager_recovery_action["shouldFireWatchdog"] = False - hm_config["hmRecoveryNotification"].append(non_state_manager_recovery_action) + indices = [i for i in range(index)] + if len(indices) > 0: + # Create one global supervision & recovery action for all processes. + global_supervision = {} + global_supervision["ruleContextKey"] = "global_supervision" + global_supervision["isSeverityCritical"] = False + global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in indices] + global_supervision["refProcesses"] = [{"index": idx} for idx in indices] + global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + hm_config["hmGlobalSupervision"].append(global_supervision) + + recovery_action = {} + recovery_action["ruleContextKey"] = "recovery_notification" + recovery_action["recoveryNotificationTimeout"] = 5000 + recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) + recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(global_supervision) + recovery_action["instanceSpecifier"] = "" + recovery_action["shouldFireWatchdog"] = False + hm_config["hmRecoveryNotification"].append(recovery_action) with open(f"{output_dir}/hm_demo.json", "w") as hm_file: json.dump(hm_config, hm_file, indent=4) From a326e02f6a96ce563f5321221ca3665c67ea309b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 16 Feb 2026 15:31:36 +0100 Subject: [PATCH 11/72] Fix time unit and some flatbuffer schema violations --- scripts/config_mapping/lifecycle_config.py | 48 +++++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 73bc29e2..c9b7046b 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -117,7 +117,7 @@ def dict_merge_recursive(dict_a, dict_b): new_config["run_targets"] = {} for run_target, run_target_config in config.get("run_targets", {}).items(): - # TODO: initial_run_target is not a dictionary, merging defautls not working for this currently + # TODO: initial_run_target is not a dictionary, merging defaults not working for this currently if run_target == "initial_run_target": new_config["run_targets"][run_target] = run_target_config else: @@ -144,7 +144,7 @@ def gen_health_monitor_config(output_dir, config): """ def get_process_type(application_type): if application_type == "STATE_MANAGER": - return "STATE_MANAGEMENT" + return "STM_PROCESS" else: return "REGULAR_PROCESS" @@ -168,11 +168,14 @@ def get_all_refProcessGroupStates(run_targets): def get_recovery_process_group_state(config): return "MainPG/" + config.get("run_targets", {}).get("final_recovery_action", {}).get("switch_run_target", {}).get("run_target", "Off") - SCHEMA_VERSION_MAJOR = 8 - SCHEMA_VERSION_MINOR = 0 + def sec_to_ms(sec : float) -> int: + return int(sec * 1000) + + HM_SCHEMA_VERSION_MAJOR = 8 + HM_SCHEMA_VERSION_MINOR = 0 hm_config = {} - hm_config["versionMajor"] = SCHEMA_VERSION_MAJOR - hm_config["versionMinor"] = SCHEMA_VERSION_MINOR + hm_config["versionMajor"] = HM_SCHEMA_VERSION_MAJOR + hm_config["versionMinor"] = HM_SCHEMA_VERSION_MINOR hm_config["process"]= [] hm_config["hmMonitorInterface"] = [] hm_config["hmSupervisionCheckpoint"] = [] @@ -191,7 +194,7 @@ def get_recovery_process_group_state(config): process["identifier"] = component_name process["processType"] = get_process_type(component_config["component_properties"]["application_profile"]["application_type"]) process["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) - process["processExecutionErrors"] = {"processExecutionError":1} + process["processExecutionErrors"] = [{"processExecutionError":1}] hm_config["process"].append(process) hmMonitorIf = {} @@ -212,7 +215,7 @@ def get_recovery_process_group_state(config): alive_supervision = {} alive_supervision["ruleContextKey"] = component_name + "_alive_supervision" alive_supervision["refCheckPointIndex"] = index - alive_supervision["aliveReferenceCycle"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["reporting_cycle"] + alive_supervision["aliveReferenceCycle"] = sec_to_ms(component_config["component_properties"]["application_profile"]["alive_supervision"]["reporting_cycle"]) alive_supervision["minAliveIndications"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["min_indications"] alive_supervision["maxAliveIndications"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["max_indications"] alive_supervision["isMinCheckDisabled"] = alive_supervision["minAliveIndications"] == 0 @@ -230,8 +233,8 @@ def get_recovery_process_group_state(config): with open(f"{output_dir}/{component_name}.json", "w") as process_file: process_config = {} - process_config["versionMajor"] = SCHEMA_VERSION_MAJOR - process_config["versionMinor"] = SCHEMA_VERSION_MINOR + process_config["versionMajor"] = HM_SCHEMA_VERSION_MAJOR + process_config["versionMinor"] = HM_SCHEMA_VERSION_MINOR process_config["process"] = [] process_config["hmMonitorInterface"] = [] process_config["hmMonitorInterface"].append(hmMonitorIf) @@ -251,7 +254,6 @@ def get_recovery_process_group_state(config): hm_config["hmGlobalSupervision"].append(global_supervision) recovery_action = {} - recovery_action["ruleContextKey"] = "recovery_notification" recovery_action["recoveryNotificationTimeout"] = 5000 recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(global_supervision) @@ -262,6 +264,30 @@ def get_recovery_process_group_state(config): with open(f"{output_dir}/hm_demo.json", "w") as hm_file: json.dump(hm_config, hm_file, indent=4) + HM_CORE_SCHEMA_VERSION_MAJOR = 3 + HM_CORE_SCHEMA_VERSION_MINOR = 0 + hmcore_config = {} + hmcore_config["versionMajor"] = HM_CORE_SCHEMA_VERSION_MAJOR + hmcore_config["versionMinor"] = HM_CORE_SCHEMA_VERSION_MINOR + hmcore_config["watchdogs"] = [] + hmcore_config["config"] = [{ + "periodicity": sec_to_ms(config.get("alive_supervision", {}).get("evaluation_cycle", 0.01)) + }] + for watchdog_name, watchdog_config in config.get("watchdogs", {}).items(): + watchdog = {} + watchdog["shortName"] = watchdog_name + watchdog["deviceFilePath"] = watchdog_config["device_file_path"] + watchdog["maxTimeout"] = sec_to_ms(watchdog_config["max_timeout"]) + watchdog["deactivateOnShutdown"] = watchdog_config["deactivate_on_shutdown"] + watchdog["hasValueDeactivateOnShutdown"] = True + watchdog["requireMagicClose"] = watchdog_config["require_magic_close"] + watchdog["hasValueRequireMagicClose"] = True + hmcore_config["watchdogs"].append(watchdog) + + with open(f"{output_dir}/hmcore.json", "w") as hm_file: + json.dump(hmcore_config, hm_file, indent=4) + + def gen_launch_manager_config(output_dir, config): """ This function generates the launch manager configuration file based on the input configuration. From 385f47d3aec16c9fb13e0ed3a1d5bef4dae6c237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 17 Feb 2026 07:53:06 +0100 Subject: [PATCH 12/72] basic Integration test for health configuration --- scripts/config_mapping/Readme.md | 1 + scripts/config_mapping/integration_tests.py | 38 +++++++++++++-------- scripts/config_mapping/lifecycle_config.py | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 0b6185e7..ff62b122 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -84,4 +84,5 @@ Open topics: * What if an object is explicitly set to {} in the config? Will this overwrite the default to None? * What about supervision and state manager? +* What is the default ready condition? diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 4fc1e644..bdaf2c4a 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -4,14 +4,12 @@ import shutil from pathlib import Path import filecmp -from lifecycle_config import preprocess_defaults -import json script_dir = Path(__file__).parent tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" -def run(input_file : Path, test_name : str, compare_files_only=[]): +def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files=[]): """ Execute the mapping script with the given input file and compare the generated output with the expected output. Input: @@ -21,6 +19,9 @@ def run(input_file : Path, test_name : str, compare_files_only=[]): actual_output_dir = tests_dir / test_name / "actual_output" expected_output_dir = tests_dir / test_name / "expected_output" + if compare_files_only and exclude_files: + raise AssertionError("You may only make use of either parameters: compare_files_only or exclude_files, but not both.") + # Clean and create actual output directory if actual_output_dir.exists(): shutil.rmtree(actual_output_dir) @@ -45,18 +46,18 @@ def run(input_file : Path, test_name : str, compare_files_only=[]): if compare_files_only: # Compare only specific files - if not compare_files(actual_output_dir, expected_output_dir, compare_files_only): + if not compare_files(actual_output_dir, expected_output_dir, compare_files_only, exclude_files): raise AssertionError("Actual output files do not match expected output files.") else: # Compare the complete directory content - if not compare_directories(actual_output_dir, expected_output_dir): + if not compare_directories(actual_output_dir, expected_output_dir, exclude_files): raise AssertionError("Actual output does not match expected output.") -def compare_directories(dir1: Path, dir2: Path) -> bool: +def compare_directories(dir1: Path, dir2: Path, exclude_files : list) -> bool: """ Compare two directories recursively. Return True if they are the same, False otherwise. """ - dcmp = filecmp.dircmp(dir1, dir2) + dcmp = filecmp.dircmp(dir1, dir2, ignore=exclude_files) if dcmp.left_only or dcmp.right_only or dcmp.diff_files: print(f"Directories differ: {dir1} vs {dir2}") @@ -84,14 +85,23 @@ def compare_files(dir1 : Path, dir2 : Path, files : list) -> bool: return True def test_basic(): + """ + Basic Smoketest for generating both launch manager and health monitoring configuration + """ test_name = "basic_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name="basic_test") + run(input_file, test_name) + +def test_health_config_mapping(): + """ + Test generation of the health monitoring configuration with + * Different application types + * Different alive supervision parameters + * Different Uid + """ + test_name = "health_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, exclude_files=["lm_demo.json"]) -#def test_health_config_mapping(): -# test_name = "health_config_test" -# input_file = tests_dir / test_name / "input" / "lm_config.json" -# -# run(input_file, test_name="basic_test", compare_files_only=["hm_demo.json"]) -# pass diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index c9b7046b..f8abed03 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -201,7 +201,7 @@ def sec_to_ms(sec : float) -> int: hmMonitorIf["instanceSpecifier"] = component_name hmMonitorIf["processShortName"] = component_name hmMonitorIf["portPrototype"] = "DefaultPort" - hmMonitorIf["interfacePath"] = "lifecycle_health" + component_name + hmMonitorIf["interfacePath"] = "lifecycle_health_" + component_name hmMonitorIf["refProcessIndex"] = index hmMonitorIf["permittedUid"] = component_config["deployment_config"]["sandbox"]["uid"] hm_config["hmMonitorInterface"].append(hmMonitorIf) From 1473e909e9b629155bab435435ec9e3c2f759164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 17 Feb 2026 07:56:23 +0100 Subject: [PATCH 13/72] Better nameing of env vars in unit test --- scripts/config_mapping/unit_tests.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py index 12dc6131..16d33e8b 100644 --- a/scripts/config_mapping/unit_tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -14,8 +14,8 @@ def test_preprocessing_basic(): "ready_timeout": 0.5, "shutdown_timeout": 0.5, "environmental_variables" : { - "DEFAULT1": "default_value1", - "DEFAULT2": "default_value2" + "global_default1": "global_default_value1", + "global_default2": "global_default_value2" }, "sandbox": { "uid": 0, @@ -39,9 +39,9 @@ def test_preprocessing_basic(): "deployment_config": { "shutdown_timeout": 1.0, "environmental_variables" : { - "DEFAULT2": "overridden_value2", - "DEFAULT3": "default_value3", - "DEFAULT4": "default_value4" + "global_default2": "config_default_overwritten_value2", + "config_default3": "config_default_value3", + "config_default4": "config_default_value4" }, "recovery_action": { "restart": { @@ -62,7 +62,7 @@ def test_preprocessing_basic(): }, "deployment_config": { "environmental_variables": { - "DEFAULT3": "overridden_value3" + "config_default3": "config_overwritten_value3" }, "sandbox": { "uid": 0, @@ -107,10 +107,10 @@ def test_preprocessing_basic(): "ready_timeout": 0.5, "shutdown_timeout": 1.0, "environmental_variables" : { - "DEFAULT1": "default_value1", - "DEFAULT2": "overridden_value2", - "DEFAULT3": "overridden_value3", - "DEFAULT4": "default_value4" + "global_default1": "global_default_value1", + "global_default2": "config_default_overwritten_value2", + "config_default3": "config_overwritten_value3", + "config_default4": "config_default_value4" }, "sandbox": { "uid": 0, From 0214f86893f57436a91a115be17105d883631397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 17 Feb 2026 08:03:35 +0100 Subject: [PATCH 14/72] Configs for health integration test --- .../expected_output/hm_demo.json | 241 ++++++++++++++++++ .../expected_output/hmcore.json | 29 +++ .../reporting_supervised_component.json | 15 ++ ...sed_component_with_no_max_indications.json | 15 ++ .../expected_output/state_manager.json | 15 ++ .../health_config_test/input/lm_config.json | 145 +++++++++++ 6 files changed, 460 insertions(+) create mode 100644 scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json create mode 100644 scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json create mode 100644 scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component.json create mode 100644 scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component_with_no_max_indications.json create mode 100644 scripts/config_mapping/tests/health_config_test/expected_output/state_manager.json create mode 100644 scripts/config_mapping/tests/health_config_test/input/lm_config.json diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json new file mode 100644 index 00000000..ae85086b --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json @@ -0,0 +1,241 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [ + { + "index": 0, + "shortName": "state_manager", + "identifier": "state_manager", + "processType": "STM_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + }, + { + "index": 1, + "shortName": "reporting_supervised_component", + "identifier": "reporting_supervised_component", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + }, + { + "index": 2, + "shortName": "reporting_supervised_component_with_no_max_indications", + "identifier": "reporting_supervised_component_with_no_max_indications", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + } + ], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 0, + "permittedUid": 4 + }, + { + "instanceSpecifier": "reporting_supervised_component", + "processShortName": "reporting_supervised_component", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component", + "refProcessIndex": 1, + "permittedUid": 5 + }, + { + "instanceSpecifier": "reporting_supervised_component_with_no_max_indications", + "processShortName": "reporting_supervised_component_with_no_max_indications", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component_with_no_max_indications", + "refProcessIndex": 2, + "permittedUid": 5 + } + ], + "hmSupervisionCheckpoint": [ + { + "shortName": "state_manager_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 0 + }, + { + "shortName": "reporting_supervised_component_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 1 + }, + { + "shortName": "reporting_supervised_component_with_no_max_indications_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 2 + } + ], + "hmAliveSupervision": [ + { + "ruleContextKey": "state_manager_alive_supervision", + "refCheckPointIndex": 0, + "aliveReferenceCycle": 100, + "minAliveIndications": 0, + "maxAliveIndications": 2, + "isMinCheckDisabled": true, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 0, + "refProcessIndex": 0, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_alive_supervision", + "refCheckPointIndex": 1, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 1, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_with_no_max_indications_alive_supervision", + "refCheckPointIndex": 2, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 0, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": true, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 2, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + } + ], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [ + { + "ruleContextKey": "state_manager_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 0 + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 1 + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_with_no_max_indications_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 2 + } + ] + } + ], + "hmGlobalSupervision": [ + { + "ruleContextKey": "global_supervision", + "isSeverityCritical": false, + "localSupervision": [ + { + "refLocalSupervisionIndex": 0 + }, + { + "refLocalSupervisionIndex": 1 + }, + { + "refLocalSupervisionIndex": 2 + } + ], + "refProcesses": [ + { + "index": 0 + }, + { + "index": 1 + }, + { + "index": 2 + } + ], + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + } + ], + "hmRecoveryNotification": [ + { + "recoveryNotificationTimeout": 5000, + "processGroupMetaModelIdentifier": "MainPG/Minimal", + "refGlobalSupervisionIndex": 0, + "instanceSpecifier": "", + "shouldFireWatchdog": false + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json new file mode 100644 index 00000000..55313469 --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json @@ -0,0 +1,29 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "simple_watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + }, + { + "shortName": "complex_watchdog", + "deviceFilePath": "/dev/watchdog2", + "maxTimeout": 1000, + "deactivateOnShutdown": false, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": true, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 123 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component.json b/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component.json new file mode 100644 index 00000000..f77030dd --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "reporting_supervised_component", + "processShortName": "reporting_supervised_component", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component", + "refProcessIndex": 1, + "permittedUid": 5 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component_with_no_max_indications.json b/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component_with_no_max_indications.json new file mode 100644 index 00000000..3846498c --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component_with_no_max_indications.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "reporting_supervised_component_with_no_max_indications", + "processShortName": "reporting_supervised_component_with_no_max_indications", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component_with_no_max_indications", + "refProcessIndex": 2, + "permittedUid": 5 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/state_manager.json b/scripts/config_mapping/tests/health_config_test/expected_output/state_manager.json new file mode 100644 index 00000000..fb4d46dd --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/state_manager.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 0, + "permittedUid": 4 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/input/lm_config.json b/scripts/config_mapping/tests/health_config_test/input/lm_config.json new file mode 100644 index 00000000..7f96b673 --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/input/lm_config.json @@ -0,0 +1,145 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + }, + "component_properties": { + "application_profile": { + "application_type": "REPORTING_AND_SUPERVISED", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "components": { + "non_supervised_comp": { + "description": "Non-supervised component", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "NATIVE", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 3 + } + } + }, + "state_manager": { + "description": "StateManager with min_indications set to 0", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "STATE_MANAGER", + "alive_supervision": { + "reporting_cycle": 0.1, + "failed_cycles_tolerance": 0, + "min_indications": 0, + "max_indications": 2 + } + } + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager", + "sandbox": { + "uid": 4 + } + } + }, + "reporting_supervised_component": { + "description": "Component reporting Running state and supervised", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "REPORTING_AND_SUPERVISED", + "is_self_terminating": true + }, + "process_arguments": ["-a", "-b"] + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 5 + } + } + }, + "reporting_supervised_component_with_no_max_indications": { + "description": "Component reporting Running state and supervised with max_indications=0", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "REPORTING_AND_SUPERVISED", + "is_self_terminating": true, + "alive_supervision": { + "max_indications": 0 + } + }, + "process_arguments": ["-a", "-b"] + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 5 + } + } + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["reporting_supervised_component", "reporting_supervised_component_with_no_max_indications", "Minimal"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Minimal" + } + } + }, + "initial_run_target": "Full", + "final_recovery_action": { + "switch_run_target": { + "run_target": "Minimal" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.123 + }, + "watchdogs": { + "simple_watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + }, + "complex_watchdog": { + "device_file_path": "/dev/watchdog2", + "max_timeout": 1, + "deactivate_on_shutdown": false, + "require_magic_close": true + } + } +} \ No newline at end of file From 6bcdeb8227ab6572a59900f0a1b96c2a2cd8e907 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Wed, 18 Feb 2026 08:26:47 +0100 Subject: [PATCH 15/72] Start work --- scripts/config_mapping/lifecycle_config.py | 108 +++++++++++++++++- .../tests/basic_test/input/lm_config.json | 4 +- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 2c3232bd..57405693 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -72,6 +72,8 @@ def preprocess_defaults(global_defaults, config): The resulting file with have no "defaults" entry anymore, but looks like if the user had specified all the fields explicitly. """ def dict_merge(dict_a, dict_b): + if dict_b is None: + return dict_a for key, value in dict_b.items(): if key in dict_a and isinstance(dict_a[key], dict) and isinstance(value, dict): # For certain dictionaries, we do not want to merge the defaults with the user specified values @@ -118,10 +120,11 @@ def dict_merge(dict_a, dict_b): if run_target == "initial_run_target": new_config["run_targets"][run_target] = run_target_config else: - new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) + # Merge into a fresh copy so we don't mutate defaults or alias run target dicts. + new_config["run_targets"][run_target] = dict_merge(deepcopy(merged_defaults["run_target"]), run_target_config) - new_config["alive_supervision"] = dict_merge(merged_defaults["alive_supervision"], config.get("alive_supervision", {})) - new_config["watchdogs"] = dict_merge(merged_defaults["watchdogs"], config.get("watchdogs", {})) + new_config["alive_supervision"] = dict_merge(deepcopy(merged_defaults["alive_supervision"]), config.get("alive_supervision", {})) + new_config["watchdogs"] = dict_merge(deepcopy(merged_defaults["watchdogs"]), config.get("watchdogs", {})) #print(json.dumps(new_config, indent=4)) @@ -226,9 +229,106 @@ def gen_launch_manager_config(output_dir, config): Output: - A file named "lm_demo.json" containing the launch manager configuration """ + def get_recovery_state(): + name = config["run_targets"]["final_recovery_action"]["switch_run_target"]["run_target"] + return "MainPG/" + name + + def get_process_dependencies(run_target): + out = [] + if not "depends_on" in run_target: + return out + for component in run_target["depends_on"]: + if component in config["components"]: + out.append(component) + if "depends_on" in config["components"][component]["component_properties"]: + # All dependencies must be components, since components can't depend on run targets + for dep in config["components"][component]["component_properties"]["depends_on"]: + if not dep in out: + out.append(dep) + out += get_process_dependencies(config["components"][dep]["component_properties"]) + else: + out += get_process_dependencies(config["run_targets"][component]) + return out + + + lm_config = {} + lm_config["versionMajor"] = 7 + lm_config["versionMinor"] = 0 + lm_config["Process"] = [] + lm_config["ModeGroup"] = [{ + "identifier": "MainPG", + "initialMode_name": "Off", + "recoveryMode_name": get_recovery_state(), + "modeDeclaration": [] + }] + + process_group_states = {} + + # Run targets can depend on components and on other run targets + for pgstate, values in config["run_targets"].items(): + if pgstate in ["initial_run_target", "final_recovery_action"]: + continue + lm_config["ModeGroup"][0]["modeDeclaration"].append({ + "identifier": "MainPG/" + pgstate + }) + components = list(set(get_process_dependencies(values))) + state_name = "MainPG/" + pgstate + for component in components: + if component not in process_group_states: + process_group_states[component] = [] + process_group_states[component].append(state_name) + + import pdb + pdb.set_trace() + + for component_name, component_config in config["components"].items(): + process = {} + process["uid"] = component_config["deployment_config"]["sandbox"]["uid"] + process["gid"] = component_config["deployment_config"]["sandbox"]["gid"] + process["sgids"] = component_config["deployment_config"]["sandbox"]["supplementary_group_ids"] + process["identifier"] = component_name + process["path"] = component_config["deployment_config"]["bin_dir"] + "/" + component_config["component_properties"]["binary_name"] + process["numberOfRestartAttempts"] = component_config["deployment_config"]["ready_recovery_action"]["restart"]["number_of_attempts"] + process["startupConfig"] = [{}] + process["startupConfig"][0]["enterTimeoutValue"] = component_config["deployment_config"]["ready_timeout"] + process["startupConfig"][0]["exitTimeoutValue"] = component_config["deployment_config"]["shutdown_timeout"] + process["startupConfig"][0]["schedulingPolicy"] = component_config["deployment_config"]["sandbox"]["scheduling_policy"] + process["startupConfig"][0]["schedulingPriority"] = component_config["deployment_config"]["sandbox"]["scheduling_priority"] + process["startupConfig"][0]["processGroupStateDependency"] = [] + process["startupConfig"][0]["environmentVariable"] = [] + for env_var, value in component_config["deployment_config"]["environmental_variables"].items(): + process["startupConfig"][0]["environmentVariable"].append({ + "key": env_var, + "value": value + }) + + if "process_arguments" in component_config: + process["startupConfig"][0]["processArgument"] = component_config["process_arguments"] + + if component_name in process_group_states: + for pgstate in process_group_states[component_name]: + process["startupConfig"][0]["processGroupStateDependency"].append({ + "stateMachine_name": "MainPG", + "stateName": pgstate + }) + + lm_config["Process"].append(process) + + # Components can never depend on run targets + for process in lm_config["Process"]: + process["executionDependency"] = [] + for dependency in config["components"][process["identifier"]]["component_properties"]["depends_on"]: + # import pdb + # pdb.set_trace() + dep_entry = config["components"][dependency] + ready_condition = dep_entry["component_properties"]["ready_condition"] + process["executionDependency"].append({ + "stateName": ready_condition, + "targetProcess_identifier": f"/{dependency}App/{dependency}" + }) + with open(f"{output_dir}/lm_demo.json", "w") as lm_file: - lm_config = {} json.dump(lm_config, lm_file, indent=4) def main(): diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index 3e2b8ebb..ec95adb5 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -72,6 +72,7 @@ "application_type": "NATIVE", "is_self_terminating": true }, + "depends_on": [], "process_arguments": ["-a", "-b"], "ready_condition": { "process_state": "Terminated" @@ -97,7 +98,8 @@ "someip-daemon": { "description": "SOME/IP application", "component_properties": { - "binary_name": "someipd" + "binary_name": "someipd", + "depends_on": [] }, "deployment_config": { "bin_dir" : "/opt/apps/someip" From ee10cbc8aeedaf253b7f99845606f1ad4bdb2e95 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Wed, 18 Feb 2026 14:23:36 +0100 Subject: [PATCH 16/72] Almost done --- scripts/config_mapping/lifecycle_config.py | 61 +++++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index c49d97e3..82f19ef7 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -300,12 +300,13 @@ def gen_launch_manager_config(output_dir, config): - A file named "lm_demo.json" containing the launch manager configuration """ def get_recovery_state(): - name = config["run_targets"]["final_recovery_action"]["switch_run_target"]["run_target"] - return "MainPG/" + name + if config.get("fallback_run_target"): + return "MainPG/Fallback" + return "MainPG/Off" def get_process_dependencies(run_target): out = [] - if not "depends_on" in run_target: + if "depends_on" not in run_target: return out for component in run_target["depends_on"]: if component in config["components"]: @@ -313,13 +314,18 @@ def get_process_dependencies(run_target): if "depends_on" in config["components"][component]["component_properties"]: # All dependencies must be components, since components can't depend on run targets for dep in config["components"][component]["component_properties"]["depends_on"]: - if not dep in out: + if dep not in out: out.append(dep) out += get_process_dependencies(config["components"][dep]["component_properties"]) else: out += get_process_dependencies(config["run_targets"][component]) return out + def get_terminating_behavior(component_config): + if component_config["component_properties"]["application_profile"]["is_self_terminating"]: + return "ProcessIsSelfTerminating" + else: + return "ProcessIsNotSelfTerminating" lm_config = {} lm_config["versionMajor"] = 7 @@ -327,16 +333,20 @@ def get_process_dependencies(run_target): lm_config["Process"] = [] lm_config["ModeGroup"] = [{ "identifier": "MainPG", - "initialMode_name": "Off", + "initialMode_name": config["initial_run_target"], "recoveryMode_name": get_recovery_state(), "modeDeclaration": [] }] process_group_states = {} + if 'Fallback' in config['run_targets']: + print('Run target name Fallback is reserved at the moment') + exit(1) + # Run targets can depend on components and on other run targets for pgstate, values in config["run_targets"].items(): - if pgstate in ["initial_run_target", "final_recovery_action"]: + if pgstate == "initial_run_target": continue lm_config["ModeGroup"][0]["modeDeclaration"].append({ "identifier": "MainPG/" + pgstate @@ -348,22 +358,33 @@ def get_process_dependencies(run_target): process_group_states[component] = [] process_group_states[component].append(state_name) - import pdb - pdb.set_trace() + fallback = config.get("fallback_run_target", {}) + if fallback: + lm_config["ModeGroup"][0]["modeDeclaration"].append({ + "identifier": "MainPG/Fallback" + }) + fallback_components = list(set(get_process_dependencies(fallback))) + for component in fallback_components: + if component not in process_group_states: + process_group_states[component] = [] + process_group_states[component].append("MainPG/Fallback") for component_name, component_config in config["components"].items(): process = {} process["uid"] = component_config["deployment_config"]["sandbox"]["uid"] process["gid"] = component_config["deployment_config"]["sandbox"]["gid"] process["sgids"] = component_config["deployment_config"]["sandbox"]["supplementary_group_ids"] + process["securityPolicyDetails"] = component_config["deployment_config"]["sandbox"]["security_policy"] process["identifier"] = component_name process["path"] = component_config["deployment_config"]["bin_dir"] + "/" + component_config["component_properties"]["binary_name"] process["numberOfRestartAttempts"] = component_config["deployment_config"]["ready_recovery_action"]["restart"]["number_of_attempts"] process["startupConfig"] = [{}] - process["startupConfig"][0]["enterTimeoutValue"] = component_config["deployment_config"]["ready_timeout"] - process["startupConfig"][0]["exitTimeoutValue"] = component_config["deployment_config"]["shutdown_timeout"] + process["startupConfig"][0]["identifier"] = f"{component_name}_startup_config" + process["startupConfig"][0]["enterTimeoutValue"] = component_config["deployment_config"]["ready_timeout"] * 1000 + process["startupConfig"][0]["exitTimeoutValue"] = component_config["deployment_config"]["shutdown_timeout"] * 1000 process["startupConfig"][0]["schedulingPolicy"] = component_config["deployment_config"]["sandbox"]["scheduling_policy"] - process["startupConfig"][0]["schedulingPriority"] = component_config["deployment_config"]["sandbox"]["scheduling_priority"] + process["startupConfig"][0]["schedulingPriority"] = str(component_config["deployment_config"]["sandbox"]["scheduling_priority"]) + process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior(component_config) process["startupConfig"][0]["processGroupStateDependency"] = [] process["startupConfig"][0]["environmentVariable"] = [] for env_var, value in component_config["deployment_config"]["environmental_variables"].items(): @@ -372,6 +393,18 @@ def get_process_dependencies(run_target): "value": value }) + match component_config["component_properties"]["application_profile"]["application_type"]: + case "Native": + process["executable_reportingBehavior"] = "DoesNotReportExecutionState" + case "State_Manager": + process["executable_reportingBehavior"] = "ReportsExecutionState" + process["functionClusterAffiliation"] = "STATE_MANAGEMENT" + case "Reporting" | "Reporting_And_Supervised": + process["executable_reportingBehavior"] = "ReportsExecutionState" + case _: + print(f'Unknown reporting behavior: {component_config["component_properties"]["application_profile"]["application_type"]}') + exit(1) + if "process_arguments" in component_config: process["startupConfig"][0]["processArgument"] = component_config["process_arguments"] @@ -386,13 +419,13 @@ def get_process_dependencies(run_target): # Components can never depend on run targets for process in lm_config["Process"]: - process["executionDependency"] = [] + process["startupConfig"][0]["executionDependency"] = [] for dependency in config["components"][process["identifier"]]["component_properties"]["depends_on"]: # import pdb # pdb.set_trace() dep_entry = config["components"][dependency] - ready_condition = dep_entry["component_properties"]["ready_condition"] - process["executionDependency"].append({ + ready_condition = dep_entry["component_properties"]["ready_condition"]["process_state"] + process["startupConfig"][0]["executionDependency"].append({ "stateName": ready_condition, "targetProcess_identifier": f"/{dependency}App/{dependency}" }) From 02510168baa502b790ab29d191b6c5b70f1c7812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 18 Feb 2026 15:43:08 +0100 Subject: [PATCH 17/72] Add validation and update sample configs --- scripts/config_mapping/lifecycle_config.py | 539 +++++++++++++++++- scripts/config_mapping/requirements.txt | 1 + .../tests/basic_test/input/lm_config.json | 23 +- .../expected_output/hm_demo.json | 2 +- .../health_config_test/input/lm_config.json | 22 +- 5 files changed, 551 insertions(+), 36 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index f8abed03..5917ec5a 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -5,6 +5,508 @@ import json from typing import Dict, Any +# TODO +json_schema = json.loads(''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "s-core_launch_manager.schema.json", + "title": "Configuration schema for the S-CORE Launch Manager", + "type": "object", + "$defs": { + "component_properties": { + "type": "object", + "description": "Defines a reusable type that captures the essential development-time characteristics of a software component.", + "properties": { + "binary_name": { + "type": "string", + "description": "Specifies the relative path of the executable file inside the directory defined by 'deployment_config.bin_dir'. The final executable path will be resolved as '{bin_dir}/{binary_name}'. Example values include simple filenames (e.g., 'test_app1') or subdirectory paths (e.g., 'bin/test_app1')." + }, + "application_profile": { + "type": "object", + "description": "Specifies the application profile that defines the runtime behavior and capabilities of this component.", + "properties": { + "application_type": { + "type": "string", + "enum": [ + "Native", + "Reporting", + "Reporting_And_Supervised", + "State_Manager" + ], + "description": "Specifies the level of integration between the component and the Launch Manager. 'Native': no integration with Launch Manager. 'Reporting': uses Launch Manager lifecycle APIs. 'Reporting_And_Supervised': uses lifecycle APIs and sends alive notifications. 'State_Manager': uses lifecycle APIs, sends alive notifications, and has permission to change the active Run Target." + }, + "is_self_terminating": { + "type": "boolean", + "description": "Indicates whether component is designed to terminate automatically once its planned tasks are completed (true), or remain running until explicitly requested to terminate by the Launch Manager (false)." + }, + "alive_supervision": { + "type": "object", + "description": "Specifies the configuration parameters used for alive monitoring of the component.", + "properties": { + "reporting_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the duration of the time interval used to verify that the component sends alive notifications, within the expected time frame." + }, + "failed_cycles_tolerance": { + "type": "integer", + "minimum": 0, + "description": "Defines the maximum number of consecutive reporting cycle failures (see 'reporting_cycle'). Once the number of failed cycles goes above maximum number, Launch Manager will trigger configured recovery action." + }, + "min_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the minimum number of checkpoints that must be reported within each configured 'reporting_cycle'." + }, + "max_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of checkpoints that may be reported within each configured 'reporting_cycle'." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "depends_on": { + "type": "array", + "description": "Names of components that this component depends on. Each dependency must be initialized and reach its ready state before this component can start.", + "items": { + "type": "string", + "description": "Specifies the name of a component on which this component depends." + } + }, + "process_arguments": { + "type": "array", + "description": "Ordered list of command-line arguments passed to the component at startup.", + "items": { + "type": "string", + "description": "Single command-line argument token as a UTF-8 string; order is preserved." + } + }, + "ready_condition": { + "type": "object", + "description": "Specifies the set of conditions that mark when the component completes its initializing state and enters the ready state.", + "properties": { + "process_state": { + "type": "string", + "enum": [ + "Running", + "Terminated" + ], + "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "recovery_action": { + "type": "object", + "description": "Defines a reusable type that specifies which recovery actions should be executed when an error or failure occurs.", + "properties": { + "restart": { + "type": "object", + "description": "Specifies a recovery action that restarts the POSIX process associated with this component.", + "properties": { + "number_of_attempts": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of restart attempts before the Launch Manager concludes that recovery cannot succeed." + }, + "delay_before_restart": { + "type": "number", + "minimum": 0, + "description": "Specifies the delay duration that Launch Manager shall wait before initiating a restart attempt." + } + }, + "required": [], + "additionalProperties": false + }, + "switch_run_target": { + "type": "object", + "description": "Specifies a recovery action that switches to a Run Target. This can be a different Run Target or the same one to retry activation of the current Run Target.", + "properties": { + "run_target": { + "type": "string", + "description": "Specifies the name of the Run Target that Launch Manager should switch to." + } + }, + "required": [], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "restart" + ] + }, + { + "required": [ + "switch_run_target" + ] + } + ], + "additionalProperties": false + }, + "deployment_config": { + "type": "object", + "description": "Defines a reusable type that contains the configuration parameters that are specific to a particular deployment environment or system setup.", + "properties": { + "ready_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time allowed for the component to reach its ready state. The timeout is measured from when the component process is created until the ready conditions specified in 'component_properties.ready_condition' are met." + }, + "shutdown_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum allowed time for the component to terminate after it receives a SIGTERM signal from the Launch Manager. The timeout is measured from the moment the Launch Manager sends the SIGTERM signal, until the Operating System notifies the Launch Manager that the child process has terminated." + }, + "environmental_variables": { + "type": "object", + "description": "Defines the set of environment variables that will be passed to the component at startup.", + "additionalProperties": { + "type": "string", + "description": "Specifies the environment variable's value as a string. An empty string is allowed and represents an intentionally empty environment variable." + } + }, + "bin_dir": { + "type": "string", + "description": "Specifies the absolute filesystem path to the directory where component is installed." + }, + "working_dir": { + "type": "string", + "description": "Specifies the directory that will be used as the working directory for the component during execution." + }, + "ready_recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "restart": true + }, + "required": [ + "restart" + ], + "not": { + "required": [ + "switch_run_target" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component fails to reach its ready state within the configured timeout." + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component malfunctions after reaching its ready state." + }, + "sandbox": { + "type": "object", + "description": "Specifies the sandbox configuration parameters that isolate and constrain the component's runtime execution.", + "properties": { + "uid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the POSIX user ID (UID) under which this component executes." + }, + "gid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the primary POSIX group ID (GID) under which this component executes." + }, + "supplementary_group_ids": { + "type": "array", + "description": "Specifies the list of supplementary POSIX group IDs (GIDs) assigned to this component.", + "items": { + "type": "integer", + "minimum": 0, + "description": "Single supplementary POSIX group ID (GID)" + } + }, + "security_policy": { + "type": "string", + "description": "Specifies the security policy or confinement profile name (such as an SELinux or AppArmor profile) assigned to the component." + }, + "scheduling_policy": { + "type": "string", + "description": "Specifies the scheduling policy applied to the component's initial thread. Supported values correspond to OS-defined policies (e.g., FIFO, RR, OTHER).", + "anyOf": [ + { + "enum": [ + "SCHED_FIFO", + "SCHED_RR", + "SCHED_OTHER" + ] + }, + { + "type": "string" + } + ] + }, + "scheduling_priority": { + "type": "integer", + "description": "Specifies the scheduling priority applied to the component's initial thread." + }, + "max_memory_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum amount of memory, in bytes, that the component is permitted to use during runtime." + }, + "max_cpu_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage of total CPU capacity." + } + }, + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_target": { + "type": "object", + "description": "Defines a reusable type that specifies the configuration parameters for a Run Target.", + "properties": { + "description": { + "type": "string", + "description": "User-defined description of the configured Run Target." + }, + "depends_on": { + "type": "array", + "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", + "exclusiveMinimum": 0 + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when a component assigned to this Run Target fails." + } + }, + "required": [ + "recovery_action" + ], + "additionalProperties": false + }, + "alive_supervision": { + "type": "object", + "description": "Defines a reusable type that contains the configuration parameters for alive supervision", + "properties": { + "evaluation_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the length of the time window used to assess incoming alive supervision reports." + } + }, + "required": [], + "additionalProperties": false + }, + "watchdog": { + "type": "object", + "description": "Defines a reusable type that contains the configuration parameters for the external watchdog.", + "properties": { + "device_file_path": { + "type": "string", + "description": "Path to the external watchdog device file (e.g., /dev/watchdog)." + }, + "max_timeout": { + "type": "number", + "minimum": 0, + "description": "Specifies the maximum timeout value that the Launch Manager will configure on the external watchdog during startup. The external watchdog uses this timeout as the deadline for receiving periodic alive reports from the Launch Manager." + }, + "deactivate_on_shutdown": { + "type": "boolean", + "description": "Specifies whether the Launch Manager disables the external watchdog during shutdown. When set to true, the watchdog is deactivated; when false, it remains active." + }, + "require_magic_close": { + "type": "boolean", + "description": "When true, the Launch Manager will perform a defined shutdown sequence to inform the external watchdog that the shutdown is intentional and to prevent a watchdog-initiated reset." + } + }, + "required": [], + "additionalProperties": false + } + }, + "properties": { + "schema_version": { + "type": "integer", + "description": "Specifies the schema version number that the Launch Manager uses to determine how to parse and validate this configuration file.", + "enum": [ + 1 + ] + }, + "defaults": { + "type": "object", + "description": "Specifies default configuration values that components and Run Targets inherit unless they provide their own overriding values.", + "properties": { + "component_properties": { + "description": "Specifies default component property values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Specifies default deployment configuration values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/deployment_config" + }, + "run_target": { + "description": "Specifies default Run Target configuration values applied to all Run Targets unless overridden in individual Run Target definitions.", + "$ref": "#/$defs/run_target" + }, + "alive_supervision": { + "description": "Specifies default alive supervision configuration values that are used unless a global 'alive_supervision' configuration is defined at the root level.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdog": { + "description": "Specifies default watchdog configuration values applied to all watchdogs unless overridden in individual watchdog definitions.", + "$ref": "#/$defs/watchdog" + } + }, + "required": [], + "additionalProperties": false + }, + "components": { + "type": "object", + "description": "Defines software components managed by the Launch Manager, where each property name is a unique component identifier and its value contains the component's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "description": "Specifies an individual component's configuration properties and deployment settings.", + "properties": { + "description": { + "type": "string", + "description": "A human-readable description of the component's purpose." + }, + "component_properties": { + "description": "Specifies component properties for this component; any properties not specified here are inherited from 'defaults.component_properties'.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Specifies deployment configuration for this component; any properties not specified here are inherited from 'defaults.deployment_config'.", + "$ref": "#/$defs/deployment_config" + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_targets": { + "type": "object", + "description": "Defines Run Targets representing different operational modes of the system, where each property name is a unique Run Target identifier and its value contains the Run Target's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "$ref": "#/$defs/run_target" + } + }, + "required": [], + "additionalProperties": false + }, + "initial_run_target": { + "type": "string", + "description": "Specifies the initial Run Target name that the Launch Manager activates during startup sequence. This name shall match a Run Target defined in 'run_targets'." + }, + "fallback_run_target": { + "type": "object", + "description": "Defines the fallback Run Target configuration that the Launch Manager activates when all recovery attempts have been exhausted. This Run Target does not include a recovery_action property.", + "properties": { + "description": { + "type": "string", + "description": "A human-readable description of the fallback Run Target." + }, + "depends_on": { + "type": "array", + "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", + "exclusiveMinimum": 0 + } + }, + "required": [], + "additionalProperties": false + }, + "alive_supervision": { + "description": "Defines alive supervision configuration parameters used to monitor component health. This configuration overrides 'defaults.alive_supervision' if specified.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdogs": { + "type": "object", + "description": "Defines external watchdog devices used by the Launch Manager, where each property name is a unique watchdog identifier and its value contains the watchdog's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "$ref": "#/$defs/watchdog" + } + }, + "required": [], + "additionalProperties": false + } + }, + "description": "Specifies the structure and valid values for the Launch Manager configuration file, which defines managed components, run targets, and recovery behaviors.", + "required": [ + "schema_version" + ], + "additionalProperties": false +} +''') + score_defaults = json.loads(''' { "deployment_config": { @@ -59,7 +561,7 @@ # There are various dictionaries in the config where only a single entry is allowed. # We do not want to merge the defaults with the user specified values for these dictionaries. -not_merging_dicts = ["ready_recovery_action", "recovery_action", "final_recovery_action"] +not_merging_dicts = ["ready_recovery_action", "recovery_action"] def load_json_file(file_path: str) -> Dict[str, Any]: """Load and parse a JSON file.""" @@ -117,11 +619,7 @@ def dict_merge_recursive(dict_a, dict_b): new_config["run_targets"] = {} for run_target, run_target_config in config.get("run_targets", {}).items(): - # TODO: initial_run_target is not a dictionary, merging defaults not working for this currently - if run_target == "initial_run_target": - new_config["run_targets"][run_target] = run_target_config - else: - new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) + new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) new_config["alive_supervision"] = dict_merge(merged_defaults["alive_supervision"], config.get("alive_supervision", {})) new_config["watchdogs"] = dict_merge(merged_defaults["watchdogs"], config.get("watchdogs", {})) @@ -143,19 +641,18 @@ def gen_health_monitor_config(output_dir, config): - For each supervised process, a file named "_.json" """ def get_process_type(application_type): - if application_type == "STATE_MANAGER": + if application_type == "State_Manager": return "STM_PROCESS" else: return "REGULAR_PROCESS" def is_supervised(application_type): - return application_type == "STATE_MANAGER" or application_type == "REPORTING_AND_SUPERVISED" + return application_type == "State_Manager" or application_type == "Reporting_And_Supervised" def get_all_process_group_states(run_targets): process_group_states = [] for run_target, _ in run_targets.items(): - if run_target not in ["initial_run_target", "final_recovery_action"]: - process_group_states.append("MainPG/"+run_target) + process_group_states.append("MainPG/"+run_target) return process_group_states def get_all_refProcessGroupStates(run_targets): @@ -166,7 +663,7 @@ def get_all_refProcessGroupStates(run_targets): return refProcessGroupStates def get_recovery_process_group_state(config): - return "MainPG/" + config.get("run_targets", {}).get("final_recovery_action", {}).get("switch_run_target", {}).get("run_target", "Off") + return "MainPG/fallback_run_target" def sec_to_ms(sec : float) -> int: return int(sec * 1000) @@ -303,13 +800,33 @@ def gen_launch_manager_config(output_dir, config): lm_config = {} json.dump(lm_config, lm_file, indent=4) + +def schema_validation(json_input, schema): + from jsonschema import validate, ValidationError + try: + validate(json_input, schema) + print("Schema Validation successful") + return True + except ValidationError as err: + print(err) + return False + def main(): parser = argparse.ArgumentParser() parser.add_argument("filename", help="The launch_manager configuration file") parser.add_argument("--output-dir", "-o", default=".", help="Output directory for generated files") + parser.add_argument("--validate", default=False, action='store_true', help="Only validate the provided configuration file against the schema and exit.") args = parser.parse_args() input_config = load_json_file(args.filename) + + validation_successful = schema_validation(input_config, json_schema) + if not validation_successful: + exit(1) + + if args.validate: + exit(0) + preprocessed_config = preprocess_defaults(score_defaults, input_config) gen_health_monitor_config(args.output_dir, preprocessed_config) gen_launch_manager_config(args.output_dir, preprocessed_config) diff --git a/scripts/config_mapping/requirements.txt b/scripts/config_mapping/requirements.txt index a7cdacfb..3d3b0be3 100644 --- a/scripts/config_mapping/requirements.txt +++ b/scripts/config_mapping/requirements.txt @@ -1,2 +1,3 @@ pytest>=7.0.0 pytest-json-report>=1.5.0 +jsonschema diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index 3e2b8ebb..7484ed10 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -36,7 +36,7 @@ "component_properties": { "binary_name": "test_app1", "application_profile": { - "application_type": "REPORTING_AND_SUPERVISED", + "application_type": "Reporting_And_Supervised", "is_self_terminating": false, "alive_supervision": { "reporting_cycle": 0.5, @@ -69,7 +69,7 @@ "component_properties": { "binary_name": "bin/setup_filesystem.sh", "application_profile": { - "application_type": "NATIVE", + "application_type": "Native", "is_self_terminating": true }, "process_arguments": ["-a", "-b"], @@ -86,7 +86,7 @@ "component_properties": { "binary_name": "dltd", "application_profile": { - "application_type": "NATIVE" + "application_type": "Native" }, "depends_on": ["setup_filesystem_sh"] }, @@ -118,7 +118,7 @@ "component_properties": { "binary_name": "sm", "application_profile": { - "application_type": "STATE_MANAGER" + "application_type": "State_Manager" }, "depends_on": ["setup_filesystem_sh"] }, @@ -146,17 +146,14 @@ "run_target": "Minimal" } } - }, - "Off": { - "description": "Nothing is running" - }, - "initial_run_target": "Minimal", - "final_recovery_action": { - "switch_run_target": { - "run_target": "Off" - } } }, + "initial_run_target": "Minimal", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, "alive_supervision" : { "evaluation_cycle": 0.5 }, diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json index ae85086b..6fd37b80 100644 --- a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json @@ -232,7 +232,7 @@ "hmRecoveryNotification": [ { "recoveryNotificationTimeout": 5000, - "processGroupMetaModelIdentifier": "MainPG/Minimal", + "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", "refGlobalSupervisionIndex": 0, "instanceSpecifier": "", "shouldFireWatchdog": false diff --git a/scripts/config_mapping/tests/health_config_test/input/lm_config.json b/scripts/config_mapping/tests/health_config_test/input/lm_config.json index 7f96b673..76c239aa 100644 --- a/scripts/config_mapping/tests/health_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/health_config_test/input/lm_config.json @@ -5,7 +5,7 @@ }, "component_properties": { "application_profile": { - "application_type": "REPORTING_AND_SUPERVISED", + "application_type": "Reporting_And_Supervised", "is_self_terminating": false, "alive_supervision": { "reporting_cycle": 0.5, @@ -25,7 +25,7 @@ "component_properties": { "binary_name": "bin/comp", "application_profile": { - "application_type": "NATIVE", + "application_type": "Native", "is_self_terminating": true }, "ready_condition": { @@ -44,7 +44,7 @@ "component_properties": { "binary_name": "sm", "application_profile": { - "application_type": "STATE_MANAGER", + "application_type": "State_Manager", "alive_supervision": { "reporting_cycle": 0.1, "failed_cycles_tolerance": 0, @@ -65,7 +65,7 @@ "component_properties": { "binary_name": "bin/comp", "application_profile": { - "application_type": "REPORTING_AND_SUPERVISED", + "application_type": "Reporting_And_Supervised", "is_self_terminating": true }, "process_arguments": ["-a", "-b"] @@ -82,7 +82,7 @@ "component_properties": { "binary_name": "bin/comp", "application_profile": { - "application_type": "REPORTING_AND_SUPERVISED", + "application_type": "Reporting_And_Supervised", "is_self_terminating": true, "alive_supervision": { "max_indications": 0 @@ -117,14 +117,14 @@ "run_target": "Minimal" } } - }, - "initial_run_target": "Full", - "final_recovery_action": { - "switch_run_target": { - "run_target": "Minimal" - } } }, + "initial_run_target": "Full", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, "alive_supervision" : { "evaluation_cycle": 0.123 }, From 05e97586957e23b06f8353dfe14b34a6213a9b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 18 Feb 2026 16:13:07 +0100 Subject: [PATCH 18/72] Add health test for empty config --- scripts/config_mapping/integration_tests.py | 10 +++ .../expected_output/hm_demo.json | 13 ++++ .../expected_output/hmcore.json | 10 +++ .../input/lm_config.json | 74 +++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 scripts/config_mapping/tests/empty_health_config_test/expected_output/hm_demo.json create mode 100644 scripts/config_mapping/tests/empty_health_config_test/expected_output/hmcore.json create mode 100644 scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index bdaf2c4a..a1ad0a5f 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -105,3 +105,13 @@ def test_health_config_mapping(): run(input_file, test_name, exclude_files=["lm_demo.json"]) +def test_empty_health_config_mapping(): + """ + Test generation of the health monitoring configuration with no supervised processes + """ + test_name = "empty_health_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, exclude_files=["lm_demo.json"]) + + diff --git a/scripts/config_mapping/tests/empty_health_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/empty_health_config_test/expected_output/hm_demo.json new file mode 100644 index 00000000..8e559853 --- /dev/null +++ b/scripts/config_mapping/tests/empty_health_config_test/expected_output/hm_demo.json @@ -0,0 +1,13 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [], + "hmSupervisionCheckpoint": [], + "hmAliveSupervision": [], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [], + "hmGlobalSupervision": [], + "hmRecoveryNotification": [] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_health_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/empty_health_config_test/expected_output/hmcore.json new file mode 100644 index 00000000..b2c87312 --- /dev/null +++ b/scripts/config_mapping/tests/empty_health_config_test/expected_output/hmcore.json @@ -0,0 +1,10 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [], + "config": [ + { + "periodicity": 123 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json new file mode 100644 index 00000000..7b9cf70c --- /dev/null +++ b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json @@ -0,0 +1,74 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "components": { + "non_supervised_comp": { + "description": "Non-supervised component", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 3 + } + } + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["non_supervised_comp"], + "recovery_action": { + "switch_run_target": { + "run_target": "Full" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["non_supervised_comp"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Minimal" + } + } + } + }, + "initial_run_target": "Full", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.123 + }, + "watchdogs": {} +} \ No newline at end of file From 22446eca1a424cdf0aaac35a4a95ed18f6ae0358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 18 Feb 2026 16:34:14 +0100 Subject: [PATCH 19/72] Update Readme file with validation info --- scripts/config_mapping/Readme.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index ff62b122..05859989 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -6,12 +6,18 @@ Once the source code of the launch_manager has been adapted to read in the new c # Usage -Providing a json file using the new configuration format as input, the script will map the content to the old configuration file format and generate those files into the specified output_dir. +Providing a json file using the new configuration format as input, the script will first validate the configuration against its schema. Then it will map the content to the old configuration file format and generate those files into the specified output_dir. ``` python3 lifecycle_config.py -o ``` +If you want to **only** validate the configuration without generating any output: + +``` +python3 lifecycle_config.py --validate +``` + # Running Tests You may want to use the virtual environment: From f0c8f0883d455006bfe6a5cf465792cbee985e30 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Thu, 19 Feb 2026 09:08:24 +0100 Subject: [PATCH 20/72] Continue config mapping script --- scripts/config_mapping/lifecycle_config.py | 76 ++++++++++++---------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index a3ecacbe..8bbd3455 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -3,6 +3,7 @@ import argparse from copy import deepcopy import json +import sys from typing import Dict, Any # TODO @@ -624,6 +625,10 @@ def dict_merge_recursive(dict_a, dict_b): new_config["alive_supervision"] = dict_merge(deepcopy(merged_defaults["alive_supervision"]), config.get("alive_supervision", {})) new_config["watchdogs"] = dict_merge(deepcopy(merged_defaults["watchdogs"]), config.get("watchdogs", {})) + for key in ("initial_run_target", "fallback_run_target"): + if key in config: + new_config[key] = config[key] + #print(json.dumps(new_config, indent=4)) return new_config @@ -795,11 +800,10 @@ def gen_launch_manager_config(output_dir, config): Output: - A file named "lm_demo.json" containing the launch manager configuration """ - def get_recovery_state(): - if config.get("fallback_run_target"): - return "MainPG/Fallback" - return "MainPG/Off" + """ + Recursively get all components on which the run target depends + """ def get_process_dependencies(run_target): out = [] if "depends_on" not in run_target: @@ -814,6 +818,7 @@ def get_process_dependencies(run_target): out.append(dep) out += get_process_dependencies(config["components"][dep]["component_properties"]) else: + # If the dependency is not a component, it must be a run target out += get_process_dependencies(config["run_targets"][component]) return out @@ -829,25 +834,23 @@ def get_terminating_behavior(component_config): lm_config["Process"] = [] lm_config["ModeGroup"] = [{ "identifier": "MainPG", - "initialMode_name": config["initial_run_target"], - "recoveryMode_name": get_recovery_state(), + "initialMode_name": config.get("initial_run_target", "Off"), + "recoveryMode_name": "MainPG/fallback_run_target", "modeDeclaration": [] }] process_group_states = {} - if 'Fallback' in config['run_targets']: - print('Run target name Fallback is reserved at the moment') + if 'fallback_run_target' in config['run_targets']: + print('Run target name fallback_run_target is reserved at the moment', file=sys.stderr) exit(1) - # Run targets can depend on components and on other run targets + # For each component, store which run targets depends on it for pgstate, values in config["run_targets"].items(): - if pgstate == "initial_run_target": - continue lm_config["ModeGroup"][0]["modeDeclaration"].append({ "identifier": "MainPG/" + pgstate }) - components = list(set(get_process_dependencies(values))) + components = set(get_process_dependencies(values)) state_name = "MainPG/" + pgstate for component in components: if component not in process_group_states: @@ -857,37 +860,23 @@ def get_terminating_behavior(component_config): fallback = config.get("fallback_run_target", {}) if fallback: lm_config["ModeGroup"][0]["modeDeclaration"].append({ - "identifier": "MainPG/Fallback" + "identifier": "MainPG/fallback_run_target" }) fallback_components = list(set(get_process_dependencies(fallback))) for component in fallback_components: if component not in process_group_states: process_group_states[component] = [] - process_group_states[component].append("MainPG/Fallback") + process_group_states[component].append("MainPG/fallback_run_target") for component_name, component_config in config["components"].items(): process = {} + process["identifier"] = component_name + process["path"] = f'{component_config["deployment_config"]["bin_dir"]}/{component_config["component_properties"]["binary_name"]}' process["uid"] = component_config["deployment_config"]["sandbox"]["uid"] process["gid"] = component_config["deployment_config"]["sandbox"]["gid"] process["sgids"] = component_config["deployment_config"]["sandbox"]["supplementary_group_ids"] process["securityPolicyDetails"] = component_config["deployment_config"]["sandbox"]["security_policy"] - process["identifier"] = component_name - process["path"] = component_config["deployment_config"]["bin_dir"] + "/" + component_config["component_properties"]["binary_name"] process["numberOfRestartAttempts"] = component_config["deployment_config"]["ready_recovery_action"]["restart"]["number_of_attempts"] - process["startupConfig"] = [{}] - process["startupConfig"][0]["identifier"] = f"{component_name}_startup_config" - process["startupConfig"][0]["enterTimeoutValue"] = component_config["deployment_config"]["ready_timeout"] * 1000 - process["startupConfig"][0]["exitTimeoutValue"] = component_config["deployment_config"]["shutdown_timeout"] * 1000 - process["startupConfig"][0]["schedulingPolicy"] = component_config["deployment_config"]["sandbox"]["scheduling_policy"] - process["startupConfig"][0]["schedulingPriority"] = str(component_config["deployment_config"]["sandbox"]["scheduling_priority"]) - process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior(component_config) - process["startupConfig"][0]["processGroupStateDependency"] = [] - process["startupConfig"][0]["environmentVariable"] = [] - for env_var, value in component_config["deployment_config"]["environmental_variables"].items(): - process["startupConfig"][0]["environmentVariable"].append({ - "key": env_var, - "value": value - }) match component_config["component_properties"]["application_profile"]["application_type"]: case "Native": @@ -901,8 +890,25 @@ def get_terminating_behavior(component_config): print(f'Unknown reporting behavior: {component_config["component_properties"]["application_profile"]["application_type"]}') exit(1) - if "process_arguments" in component_config: - process["startupConfig"][0]["processArgument"] = component_config["process_arguments"] + process["startupConfig"] = [{}] + process["startupConfig"][0]["executionError"] = "1" + process["startupConfig"][0]["identifier"] = f"{component_name}_startup_config" + process["startupConfig"][0]["enterTimeoutValue"] = int(component_config["deployment_config"]["ready_timeout"] * 1000) + process["startupConfig"][0]["exitTimeoutValue"] = int(component_config["deployment_config"]["shutdown_timeout"] * 1000) + process["startupConfig"][0]["schedulingPolicy"] = component_config["deployment_config"]["sandbox"]["scheduling_policy"] + process["startupConfig"][0]["schedulingPriority"] = str(component_config["deployment_config"]["sandbox"]["scheduling_priority"]) + process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior(component_config) + process["startupConfig"][0]["processGroupStateDependency"] = [] + process["startupConfig"][0]["environmentVariable"] = [] + for env_var, value in component_config["deployment_config"].get("environmental_variables",[]).items(): + process["startupConfig"][0]["environmentVariable"].append({ + "key": env_var, + "value": value + }) + + if (arguments := component_config.get("process_arguments", [])): + arguments = [{"argument": arg} for arg in arguments] + process["startupConfig"][0]["processArgument"] = arguments if component_name in process_group_states: for pgstate in process_group_states[component_name]: @@ -913,12 +919,10 @@ def get_terminating_behavior(component_config): lm_config["Process"].append(process) - # Components can never depend on run targets + # Execution dependencies. Assumption: Components can never depend on run targets for process in lm_config["Process"]: process["startupConfig"][0]["executionDependency"] = [] - for dependency in config["components"][process["identifier"]]["component_properties"]["depends_on"]: - # import pdb - # pdb.set_trace() + for dependency in config["components"][process["identifier"]]["component_properties"].get("depends_on", []): dep_entry = config["components"][dependency] ready_condition = dep_entry["component_properties"]["ready_condition"]["process_state"] process["startupConfig"][0]["executionDependency"].append({ From bc751d1804f77196424fb973eee12d59f021d068 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Thu, 19 Feb 2026 09:32:10 +0100 Subject: [PATCH 21/72] Cleanup --- scripts/config_mapping/lifecycle_config.py | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 8bbd3455..6e8fe88f 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -569,6 +569,16 @@ def load_json_file(file_path: str) -> Dict[str, Any]: with open(file_path, 'r') as file: return json.load(file) +def get_recovery_process_group_state(config): + if "fallback_run_target" in config: + return "MainPG/fallback_run_target" + else: + return "MainPG/Off" + +def sec_to_ms(sec : float) -> int: + return int(sec * 1000) + + def preprocess_defaults(global_defaults, config): """ This function takes the input configuration and fills in any missing fields with default values. @@ -667,12 +677,6 @@ def get_all_refProcessGroupStates(run_targets): refProcessGroupStates.append({"identifier": state}) return refProcessGroupStates - def get_recovery_process_group_state(config): - return "MainPG/fallback_run_target" - - def sec_to_ms(sec : float) -> int: - return int(sec * 1000) - HM_SCHEMA_VERSION_MAJOR = 8 HM_SCHEMA_VERSION_MINOR = 0 hm_config = {} @@ -828,6 +832,10 @@ def get_terminating_behavior(component_config): else: return "ProcessIsNotSelfTerminating" + if 'fallback_run_target' in config['run_targets']: + print('Run target name fallback_run_target is reserved at the moment', file=sys.stderr) + exit(1) + lm_config = {} lm_config["versionMajor"] = 7 lm_config["versionMinor"] = 0 @@ -835,30 +843,25 @@ def get_terminating_behavior(component_config): lm_config["ModeGroup"] = [{ "identifier": "MainPG", "initialMode_name": config.get("initial_run_target", "Off"), - "recoveryMode_name": "MainPG/fallback_run_target", + "recoveryMode_name": get_recovery_process_group_state(config), "modeDeclaration": [] }] process_group_states = {} - if 'fallback_run_target' in config['run_targets']: - print('Run target name fallback_run_target is reserved at the moment', file=sys.stderr) - exit(1) - # For each component, store which run targets depends on it for pgstate, values in config["run_targets"].items(): + state_name = "MainPG/" + pgstate lm_config["ModeGroup"][0]["modeDeclaration"].append({ - "identifier": "MainPG/" + pgstate + "identifier": state_name }) components = set(get_process_dependencies(values)) - state_name = "MainPG/" + pgstate for component in components: if component not in process_group_states: process_group_states[component] = [] process_group_states[component].append(state_name) - fallback = config.get("fallback_run_target", {}) - if fallback: + if (fallback := config.get("fallback_run_target", {})): lm_config["ModeGroup"][0]["modeDeclaration"].append({ "identifier": "MainPG/fallback_run_target" }) @@ -886,15 +889,12 @@ def get_terminating_behavior(component_config): process["functionClusterAffiliation"] = "STATE_MANAGEMENT" case "Reporting" | "Reporting_And_Supervised": process["executable_reportingBehavior"] = "ReportsExecutionState" - case _: - print(f'Unknown reporting behavior: {component_config["component_properties"]["application_profile"]["application_type"]}') - exit(1) process["startupConfig"] = [{}] process["startupConfig"][0]["executionError"] = "1" process["startupConfig"][0]["identifier"] = f"{component_name}_startup_config" - process["startupConfig"][0]["enterTimeoutValue"] = int(component_config["deployment_config"]["ready_timeout"] * 1000) - process["startupConfig"][0]["exitTimeoutValue"] = int(component_config["deployment_config"]["shutdown_timeout"] * 1000) + process["startupConfig"][0]["enterTimeoutValue"] = sec_to_ms(component_config["deployment_config"]["ready_timeout"]) + process["startupConfig"][0]["exitTimeoutValue"] = sec_to_ms(component_config["deployment_config"]["shutdown_timeout"]) process["startupConfig"][0]["schedulingPolicy"] = component_config["deployment_config"]["sandbox"]["scheduling_policy"] process["startupConfig"][0]["schedulingPriority"] = str(component_config["deployment_config"]["sandbox"]["scheduling_priority"]) process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior(component_config) From 619b50e866921b40937d06d57c5fc773bb6dabba Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Thu, 19 Feb 2026 09:49:53 +0100 Subject: [PATCH 22/72] Fix error in mapping script --- scripts/config_mapping/lifecycle_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 6e8fe88f..615da002 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -906,7 +906,7 @@ def get_terminating_behavior(component_config): "value": value }) - if (arguments := component_config.get("process_arguments", [])): + if (arguments := component_config["component_properties"].get("process_arguments", [])): arguments = [{"argument": arg} for arg in arguments] process["startupConfig"][0]["processArgument"] = arguments From 839caef839a0d586dc3156f112cb4cf4b198922d Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Thu, 19 Feb 2026 12:54:42 +0100 Subject: [PATCH 23/72] Minor fixes --- scripts/config_mapping/integration_tests.py | 27 ++ scripts/config_mapping/lifecycle_config.py | 13 +- .../basic_test/expected_output/hm_demo.json | 242 +++++++++- .../basic_test/expected_output/hmcore.json | 20 + .../basic_test/expected_output/lm_demo.json | 365 ++++++++++++++- .../expected_output/someip-daemon.json | 15 + .../expected_output/state_manager.json | 15 + .../basic_test/expected_output/test_app1.json | 15 + .../expected_output/hm_demo.json | 13 + .../expected_output/hmcore.json | 20 + .../expected_output/lm_demo.json | 17 + .../empty_lm_config_test/input/lm_config.json | 83 ++++ .../expected_output/hm_demo.json | 307 ++++++++++++ .../expected_output/hmcore.json | 20 + .../expected_output/lm_demo.json | 437 ++++++++++++++++++ .../expected_output/someip-daemon.json | 15 + .../expected_output/state_manager.json | 15 + .../expected_output/test_app1.json | 15 + .../expected_output/test_app2.json | 15 + .../tests/lm_config_test/input/lm_config.json | 193 ++++++++ 20 files changed, 1854 insertions(+), 8 deletions(-) create mode 100644 scripts/config_mapping/tests/basic_test/expected_output/hmcore.json create mode 100644 scripts/config_mapping/tests/basic_test/expected_output/someip-daemon.json create mode 100644 scripts/config_mapping/tests/basic_test/expected_output/state_manager.json create mode 100644 scripts/config_mapping/tests/basic_test/expected_output/test_app1.json create mode 100644 scripts/config_mapping/tests/empty_lm_config_test/expected_output/hm_demo.json create mode 100644 scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json create mode 100644 scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json create mode 100644 scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json create mode 100644 scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json create mode 100644 scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json create mode 100644 scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json create mode 100644 scripts/config_mapping/tests/lm_config_test/expected_output/someip-daemon.json create mode 100644 scripts/config_mapping/tests/lm_config_test/expected_output/state_manager.json create mode 100644 scripts/config_mapping/tests/lm_config_test/expected_output/test_app1.json create mode 100644 scripts/config_mapping/tests/lm_config_test/expected_output/test_app2.json create mode 100644 scripts/config_mapping/tests/lm_config_test/input/lm_config.json diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index a1ad0a5f..8ab8de87 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -9,6 +9,14 @@ tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" +def all_files_except(relative_path, filename): + """ + Helper function to specify all files in a directory except a specific file for comparison. + Usage: all_files_except("subdir", "file_to_exclude.json") + """ + dir_path = tests_dir / relative_path + return [f.name for f in dir_path.iterdir() if f.is_file() and f.name != filename] + def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files=[]): """ Execute the mapping script with the given input file and compare the generated output with the expected output. @@ -114,4 +122,23 @@ def test_empty_health_config_mapping(): run(input_file, test_name, exclude_files=["lm_demo.json"]) +def test_launch_config_mapping(): + """ + Test generation of the launch manager configuration with + * Different application types + * Different dependency configurations + * Different ready conditions + """ + test_name = "lm_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, exclude_files=all_files_except(tests_dir / test_name / "expected_output", "lm_demo.json")) +def test_empty_launch_config_mapping(): + """ + Test generation of the launch manager configuration with no processes defined + """ + test_name = "empty_lm_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, exclude_files=all_files_except(tests_dir / test_name / "expected_output", "lm_demo.json")) \ No newline at end of file diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 615da002..5e283889 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -570,7 +570,8 @@ def load_json_file(file_path: str) -> Dict[str, Any]: return json.load(file) def get_recovery_process_group_state(config): - if "fallback_run_target" in config: + fallback = config.get("fallback_run_target", None) + if fallback: return "MainPG/fallback_run_target" else: return "MainPG/Off" @@ -632,8 +633,8 @@ def dict_merge_recursive(dict_a, dict_b): for run_target, run_target_config in config.get("run_targets", {}).items(): new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) - new_config["alive_supervision"] = dict_merge(deepcopy(merged_defaults["alive_supervision"]), config.get("alive_supervision", {})) - new_config["watchdogs"] = dict_merge(deepcopy(merged_defaults["watchdogs"]), config.get("watchdogs", {})) + new_config["alive_supervision"] = dict_merge(merged_defaults["alive_supervision"], config.get("alive_supervision", {})) + new_config["watchdogs"] = dict_merge(merged_defaults["watchdogs"], config.get("watchdogs", {})) for key in ("initial_run_target", "fallback_run_target"): if key in config: @@ -877,7 +878,7 @@ def get_terminating_behavior(component_config): process["path"] = f'{component_config["deployment_config"]["bin_dir"]}/{component_config["component_properties"]["binary_name"]}' process["uid"] = component_config["deployment_config"]["sandbox"]["uid"] process["gid"] = component_config["deployment_config"]["sandbox"]["gid"] - process["sgids"] = component_config["deployment_config"]["sandbox"]["supplementary_group_ids"] + process["sgids"] = [{"sgid": sgid} for sgid in component_config["deployment_config"]["sandbox"]["supplementary_group_ids"]] process["securityPolicyDetails"] = component_config["deployment_config"]["sandbox"]["security_policy"] process["numberOfRestartAttempts"] = component_config["deployment_config"]["ready_recovery_action"]["restart"]["number_of_attempts"] @@ -900,7 +901,7 @@ def get_terminating_behavior(component_config): process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior(component_config) process["startupConfig"][0]["processGroupStateDependency"] = [] process["startupConfig"][0]["environmentVariable"] = [] - for env_var, value in component_config["deployment_config"].get("environmental_variables",[]).items(): + for env_var, value in component_config["deployment_config"].get("environmental_variables",{}).items(): process["startupConfig"][0]["environmentVariable"].append({ "key": env_var, "value": value @@ -927,7 +928,7 @@ def get_terminating_behavior(component_config): ready_condition = dep_entry["component_properties"]["ready_condition"]["process_state"] process["startupConfig"][0]["executionDependency"].append({ "stateName": ready_condition, - "targetProcess_identifier": f"/{dependency}App/{dependency}" + "targetProcess_identifier": dependency }) diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json index 9e26dfee..6d81447f 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json @@ -1 +1,241 @@ -{} \ No newline at end of file +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [ + { + "index": 0, + "shortName": "someip-daemon", + "identifier": "someip-daemon", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + }, + { + "index": 1, + "shortName": "test_app1", + "identifier": "test_app1", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + }, + { + "index": 2, + "shortName": "state_manager", + "identifier": "state_manager", + "processType": "STM_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + } + ], + "hmMonitorInterface": [ + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 0, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 1, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 2, + "permittedUid": 1000 + } + ], + "hmSupervisionCheckpoint": [ + { + "shortName": "someip-daemon_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 0 + }, + { + "shortName": "test_app1_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 1 + }, + { + "shortName": "state_manager_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 2 + } + ], + "hmAliveSupervision": [ + { + "ruleContextKey": "someip-daemon_alive_supervision", + "refCheckPointIndex": 0, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 0, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + }, + { + "ruleContextKey": "test_app1_alive_supervision", + "refCheckPointIndex": 1, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 1, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + }, + { + "ruleContextKey": "state_manager_alive_supervision", + "refCheckPointIndex": 2, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 2, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + } + ], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [ + { + "ruleContextKey": "someip-daemon_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 0 + } + ] + }, + { + "ruleContextKey": "test_app1_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 1 + } + ] + }, + { + "ruleContextKey": "state_manager_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 2 + } + ] + } + ], + "hmGlobalSupervision": [ + { + "ruleContextKey": "global_supervision", + "isSeverityCritical": false, + "localSupervision": [ + { + "refLocalSupervisionIndex": 0 + }, + { + "refLocalSupervisionIndex": 1 + }, + { + "refLocalSupervisionIndex": 2 + } + ], + "refProcesses": [ + { + "index": 0 + }, + { + "index": 1 + }, + { + "index": 2 + } + ], + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + } + ], + "hmRecoveryNotification": [ + { + "recoveryNotificationTimeout": 5000, + "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", + "refGlobalSupervisionIndex": 0, + "instanceSpecifier": "", + "shouldFireWatchdog": false + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json new file mode 100644 index 00000000..88444ba2 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "simple_watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 500 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json index 9e26dfee..03bf7270 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -1 +1,364 @@ -{} \ No newline at end of file +{ + "versionMajor": 7, + "versionMinor": 0, + "Process": [ + { + "identifier": "setup_filesystem_sh", + "path": "/opt/scripts/bin/setup_filesystem.sh", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "setup_filesystem_sh_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Minimal" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "dlt-daemon", + "path": "/opt/apps/dlt-daemon/dltd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "dlt-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + }, + { + "identifier": "someip-daemon", + "path": "/opt/apps/someip/someipd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "someip-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "test_app1", + "path": "/opt/apps/test_app1/test_app1", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "test_app1_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Running", + "targetProcess_identifier": "dlt-daemon" + }, + { + "stateName": "Running", + "targetProcess_identifier": "someip-daemon" + } + ] + } + ] + }, + { + "identifier": "state_manager", + "path": "/opt/apps/state_manager/sm", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "functionClusterAffiliation": "STATE_MANAGEMENT", + "startupConfig": [ + { + "executionError": "1", + "identifier": "state_manager_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Minimal" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + } + ], + "ModeGroup": [ + { + "identifier": "MainPG", + "initialMode_name": "Minimal", + "recoveryMode_name": "MainPG/fallback_run_target", + "modeDeclaration": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/someip-daemon.json b/scripts/config_mapping/tests/basic_test/expected_output/someip-daemon.json new file mode 100644 index 00000000..724551f0 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/someip-daemon.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 0, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/state_manager.json b/scripts/config_mapping/tests/basic_test/expected_output/state_manager.json new file mode 100644 index 00000000..dd4dfdfe --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/state_manager.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 2, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/test_app1.json b/scripts/config_mapping/tests/basic_test/expected_output/test_app1.json new file mode 100644 index 00000000..2de62915 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/test_app1.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 1, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hm_demo.json new file mode 100644 index 00000000..8e559853 --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hm_demo.json @@ -0,0 +1,13 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [], + "hmSupervisionCheckpoint": [], + "hmAliveSupervision": [], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [], + "hmGlobalSupervision": [], + "hmRecoveryNotification": [] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json new file mode 100644 index 00000000..88444ba2 --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "simple_watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 500 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json new file mode 100644 index 00000000..17f13a0f --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json @@ -0,0 +1,17 @@ +{ + "versionMajor": 7, + "versionMinor": 0, + "Process": [], + "ModeGroup": [ + { + "identifier": "MainPG", + "initialMode_name": "Minimal", + "recoveryMode_name": "MainPG/fallback_run_target", + "modeDeclaration": [ + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json new file mode 100644 index 00000000..5804c057 --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json @@ -0,0 +1,83 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib", + "GLOBAL_ENV_VAR": "abc", + "EMPTY_GLOBAL_ENV_VAR": "" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [500, 600, 700], + "security_policy": "policy_name", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + "max_memory_usage": 1024, + "max_cpu_usage": 75 + } + }, + "component_properties": { + "binary_name": "test_app1", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": ["test_app2", "test_app3"], + "process_arguments": ["-a", "-b", "--xyz"], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "initial_run_target": "Minimal", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdogs": { + "simple_watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + } +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json new file mode 100644 index 00000000..b423bc8d --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json @@ -0,0 +1,307 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [ + { + "index": 0, + "shortName": "test_app2", + "identifier": "test_app2", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + }, + { + "index": 1, + "shortName": "someip-daemon", + "identifier": "someip-daemon", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + }, + { + "index": 2, + "shortName": "test_app1", + "identifier": "test_app1", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + }, + { + "index": 3, + "shortName": "state_manager", + "identifier": "state_manager", + "processType": "STM_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + } + ] + } + ], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app2", + "processShortName": "test_app2", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app2", + "refProcessIndex": 0, + "permittedUid": 2000 + }, + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 1, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 2, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 3, + "permittedUid": 1000 + } + ], + "hmSupervisionCheckpoint": [ + { + "shortName": "test_app2_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 0 + }, + { + "shortName": "someip-daemon_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 1 + }, + { + "shortName": "test_app1_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 2 + }, + { + "shortName": "state_manager_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 3 + } + ], + "hmAliveSupervision": [ + { + "ruleContextKey": "test_app2_alive_supervision", + "refCheckPointIndex": 0, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 0, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + }, + { + "ruleContextKey": "someip-daemon_alive_supervision", + "refCheckPointIndex": 1, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 1, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + }, + { + "ruleContextKey": "test_app1_alive_supervision", + "refCheckPointIndex": 2, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 2, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + }, + { + "ruleContextKey": "state_manager_alive_supervision", + "refCheckPointIndex": 3, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 3, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + } + ], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [ + { + "ruleContextKey": "test_app2_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 0 + } + ] + }, + { + "ruleContextKey": "someip-daemon_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 1 + } + ] + }, + { + "ruleContextKey": "test_app1_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 2 + } + ] + }, + { + "ruleContextKey": "state_manager_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 3 + } + ] + } + ], + "hmGlobalSupervision": [ + { + "ruleContextKey": "global_supervision", + "isSeverityCritical": false, + "localSupervision": [ + { + "refLocalSupervisionIndex": 0 + }, + { + "refLocalSupervisionIndex": 1 + }, + { + "refLocalSupervisionIndex": 2 + }, + { + "refLocalSupervisionIndex": 3 + } + ], + "refProcesses": [ + { + "index": 0 + }, + { + "index": 1 + }, + { + "index": 2 + }, + { + "index": 3 + } + ], + "refProcessGroupStates": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + } + ] + } + ], + "hmRecoveryNotification": [ + { + "recoveryNotificationTimeout": 5000, + "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", + "refGlobalSupervisionIndex": 0, + "instanceSpecifier": "", + "shouldFireWatchdog": false + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json new file mode 100644 index 00000000..88444ba2 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "simple_watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 500 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json new file mode 100644 index 00000000..ec608cef --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json @@ -0,0 +1,437 @@ +{ + "versionMajor": 7, + "versionMinor": 0, + "Process": [ + { + "identifier": "test_app2", + "path": "/opt/apps/test_app2/test_app2", + "uid": 2000, + "gid": 2000, + "sgids": [ + { + "sgid": 800 + }, + { + "sgid": 900 + } + ], + "securityPolicyDetails": "policy_name_2", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "test_app2_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_FIFO", + "schedulingPriority": "99", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Minimal" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "overridden_value" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + }, + { + "key": "APP_SPECIFIC_ENV_VAR", + "value": "def" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "setup_filesystem_sh", + "path": "/opt/scripts/bin/setup_filesystem.sh", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "setup_filesystem_sh_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Minimal" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + } + ], + "executionDependency": [ + { + "stateName": "Running", + "targetProcess_identifier": "test_app2" + } + ] + } + ] + }, + { + "identifier": "dlt-daemon", + "path": "/opt/apps/dlt-daemon/dltd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "dlt-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + }, + { + "identifier": "someip-daemon", + "path": "/opt/apps/someip/someipd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "someip-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "test_app1", + "path": "/opt/apps/test_app1/test_app1", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "test_app1_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Running", + "targetProcess_identifier": "dlt-daemon" + }, + { + "stateName": "Running", + "targetProcess_identifier": "someip-daemon" + } + ] + } + ] + }, + { + "identifier": "state_manager", + "path": "/opt/apps/state_manager/sm", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "functionClusterAffiliation": "STATE_MANAGEMENT", + "startupConfig": [ + { + "executionError": "1", + "identifier": "state_manager_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Minimal" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + } + ], + "ModeGroup": [ + { + "identifier": "MainPG", + "initialMode_name": "Minimal", + "recoveryMode_name": "MainPG/fallback_run_target", + "modeDeclaration": [ + { + "identifier": "MainPG/Minimal" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/someip-daemon.json b/scripts/config_mapping/tests/lm_config_test/expected_output/someip-daemon.json new file mode 100644 index 00000000..bb905848 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/someip-daemon.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 1, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/state_manager.json b/scripts/config_mapping/tests/lm_config_test/expected_output/state_manager.json new file mode 100644 index 00000000..d06eca18 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/state_manager.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 3, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/test_app1.json b/scripts/config_mapping/tests/lm_config_test/expected_output/test_app1.json new file mode 100644 index 00000000..c1ee2466 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/test_app1.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 2, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/test_app2.json b/scripts/config_mapping/tests/lm_config_test/expected_output/test_app2.json new file mode 100644 index 00000000..f06af1ec --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/test_app2.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app2", + "processShortName": "test_app2", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app2", + "refProcessIndex": 0, + "permittedUid": 2000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json new file mode 100644 index 00000000..90fe43d5 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json @@ -0,0 +1,193 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib", + "GLOBAL_ENV_VAR": "abc", + "EMPTY_GLOBAL_ENV_VAR": "" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [500, 600, 700], + "security_policy": "policy_name", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + "max_memory_usage": 1024, + "max_cpu_usage": 75 + } + }, + "component_properties": { + "binary_name": "test_app1", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": ["test_app2"], + "process_arguments": ["-a", "-b", "--xyz"], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "components": { + "test_app2": { + "description": "Another simple test application", + "component_properties": { + "binary_name": "test_app2", + "depends_on": [] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app2", + "environmental_variables": { + "GLOBAL_ENV_VAR": "overridden_value", + "APP_SPECIFIC_ENV_VAR": "def" + }, + "sandbox": { + "uid": 2000, + "gid": 2000, + "supplementary_group_ids": [800, 900], + "security_policy": "policy_name_2", + "scheduling_policy": "SCHED_FIFO", + "scheduling_priority": 99, + "max_memory_usage": 2048, + "max_cpu_usage": 50 + } + } + }, + "setup_filesystem_sh": { + "description": "Script to mount partitions at the right directories", + "component_properties": { + "binary_name": "bin/setup_filesystem.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "process_arguments": ["-a", "-b"], + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts" + } + }, + "dlt-daemon": { + "description": "Logging application", + "component_properties": { + "binary_name": "dltd", + "application_profile": { + "application_type": "Native" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/dlt-daemon" + } + }, + "someip-daemon": { + "description": "SOME/IP application", + "component_properties": { + "binary_name": "someipd", + "depends_on": [] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/someip" + } + }, + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1", + "depends_on": ["dlt-daemon", "someip-daemon"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + }, + "state_manager": { + "description": "Application that manages life cycle of the ECU", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "State_Manager" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager" + } + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["test_app1", "Minimal"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Minimal" + } + } + } + }, + "initial_run_target": "Minimal", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdogs": { + "simple_watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + } +} \ No newline at end of file From 625eff43750bc9ead07163e07fb4aa6de51cad42 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Thu, 19 Feb 2026 13:03:38 +0100 Subject: [PATCH 24/72] Cleanup --- scripts/config_mapping/integration_tests.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 8ab8de87..7db174fd 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -9,14 +9,6 @@ tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" -def all_files_except(relative_path, filename): - """ - Helper function to specify all files in a directory except a specific file for comparison. - Usage: all_files_except("subdir", "file_to_exclude.json") - """ - dir_path = tests_dir / relative_path - return [f.name for f in dir_path.iterdir() if f.is_file() and f.name != filename] - def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files=[]): """ Execute the mapping script with the given input file and compare the generated output with the expected output. @@ -54,7 +46,7 @@ def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files if compare_files_only: # Compare only specific files - if not compare_files(actual_output_dir, expected_output_dir, compare_files_only, exclude_files): + if not compare_files(actual_output_dir, expected_output_dir, compare_files_only): raise AssertionError("Actual output files do not match expected output files.") else: # Compare the complete directory content @@ -132,7 +124,7 @@ def test_launch_config_mapping(): test_name = "lm_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name, exclude_files=all_files_except(tests_dir / test_name / "expected_output", "lm_demo.json")) + run(input_file, test_name, compare_files_only=["lm_demo.json"]) def test_empty_launch_config_mapping(): """ @@ -141,4 +133,4 @@ def test_empty_launch_config_mapping(): test_name = "empty_lm_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name, exclude_files=all_files_except(tests_dir / test_name / "expected_output", "lm_demo.json")) \ No newline at end of file + run(input_file, test_name, compare_files_only=["lm_demo.json"]) \ No newline at end of file From 58cc48e8a1303203d624938573b70f02898345a3 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Thu, 19 Feb 2026 14:05:43 +0100 Subject: [PATCH 25/72] Prevent cyclic dependencies --- scripts/config_mapping/lifecycle_config.py | 85 ++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 5e283889..a9e7beed 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -809,23 +809,82 @@ def gen_launch_manager_config(output_dir, config): """ Recursively get all components on which the run target depends """ - def get_process_dependencies(run_target): + def format_dependency_path(path, cycle_target): + """Format a dependency resolution path for display, highlighting the cycle.""" + return " -> ".join(path + [cycle_target]) + + def get_process_dependencies(run_target, ancestors_run_targets=None, ancestors_components=None): + """ + Resolve all component dependencies for the given run target. + + ancestors_run_targets and ancestors_components track the current + recursion path to detect cyclic dependencies without rejecting + legitimate diamond-shaped dependency trees. + """ + if ancestors_run_targets is None: + ancestors_run_targets = [] + if ancestors_components is None: + ancestors_components = [] + out = [] if "depends_on" not in run_target: return out - for component in run_target["depends_on"]: - if component in config["components"]: - out.append(component) - if "depends_on" in config["components"][component]["component_properties"]: + + for dependency_name in run_target["depends_on"]: + if dependency_name in config["components"]: + if dependency_name in ancestors_components: + path = format_dependency_path(ancestors_components, dependency_name) + raise ValueError( + f"Cyclic dependency detected: component '{dependency_name}' " + f"has already been visited.\n Path: {path}" + ) + ancestors_components.append(dependency_name) + out.append(dependency_name) + + component_props = config["components"][dependency_name]["component_properties"] + if "depends_on" in component_props: # All dependencies must be components, since components can't depend on run targets - for dep in config["components"][component]["component_properties"]["depends_on"]: - if dep not in out: - out.append(dep) - out += get_process_dependencies(config["components"][dep]["component_properties"]) + for dep in component_props["depends_on"]: + if dep not in config["components"]: + raise ValueError( + f"Component '{dependency_name}' depends on unknown component '{dep}'." + ) + if dep in ancestors_components: + path = format_dependency_path(ancestors_components, dep) + raise ValueError( + f"Cyclic dependency detected: component '{dependency_name}' " + f"depends on already visited component '{dep}'.\n Path: {path}" + ) + ancestors_components.append(dep) + out.append(dep) + out += get_process_dependencies( + config["components"][dep]["component_properties"], + ancestors_run_targets=ancestors_run_targets, + ancestors_components=ancestors_components, + ) + ancestors_components.pop() + + ancestors_components.pop() else: # If the dependency is not a component, it must be a run target - out += get_process_dependencies(config["run_targets"][component]) - return out + if dependency_name not in config["run_targets"]: + raise ValueError( + f"Run target depends on unknown run target '{dependency_name}'." + ) + if dependency_name in ancestors_run_targets: + path = format_dependency_path(ancestors_run_targets, dependency_name) + raise ValueError( + f"Cyclic dependency detected: run target '{dependency_name}' " + f"has already been visited.\n Path: {path}" + ) + ancestors_run_targets.append(dependency_name) + out += get_process_dependencies( + config["run_targets"][dependency_name], + ancestors_run_targets=ancestors_run_targets, + ancestors_components=ancestors_components, + ) + ancestors_run_targets.pop() + return list(set(out)) # Remove duplicates def get_terminating_behavior(component_config): if component_config["component_properties"]["application_profile"]["is_self_terminating"]: @@ -856,7 +915,7 @@ def get_terminating_behavior(component_config): lm_config["ModeGroup"][0]["modeDeclaration"].append({ "identifier": state_name }) - components = set(get_process_dependencies(values)) + components = get_process_dependencies(values) for component in components: if component not in process_group_states: process_group_states[component] = [] @@ -866,7 +925,7 @@ def get_terminating_behavior(component_config): lm_config["ModeGroup"][0]["modeDeclaration"].append({ "identifier": "MainPG/fallback_run_target" }) - fallback_components = list(set(get_process_dependencies(fallback))) + fallback_components = get_process_dependencies(fallback) for component in fallback_components: if component not in process_group_states: process_group_states[component] = [] From 08c2665a70a12c1331261dcff582a2bf77b4da51 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 07:53:50 +0100 Subject: [PATCH 26/72] First changes --- examples/config/gen_lifecycle_config.py | 232 +++++++++++++++++++++ examples/demo.sh | 21 +- examples/run.sh | 27 +-- scripts/config_mapping/lifecycle_config.py | 63 ++++-- 4 files changed, 310 insertions(+), 33 deletions(-) create mode 100644 examples/config/gen_lifecycle_config.py diff --git a/examples/config/gen_lifecycle_config.py b/examples/config/gen_lifecycle_config.py new file mode 100644 index 00000000..2b824fdd --- /dev/null +++ b/examples/config/gen_lifecycle_config.py @@ -0,0 +1,232 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +""" +Generates a unified lifecycle configuration file in the S-CORE Launch Manager schema format. + +This replaces the previous gen_health_monitor_cfg.py, gen_health_monitor_process_cfg.py, +and gen_launch_manager_cfg.py scripts. The generated configuration can be converted to +the old format by running: + + python3 scripts/config_mapping/lifecycle_config.py -o +""" +import argparse +import json +import os +from pathlib import Path + + +def is_rust_app(process_index: int, cppprocess_count: int, rustprocess_count: int): + processes_per_process_group = cppprocess_count + rustprocess_count + index_in_group = process_index % processes_per_process_group + return index_in_group >= cppprocess_count + + +def gen_lifecycle_config( + cppprocesses: int, + rustprocesses: int, + non_supervised_processes: int, +): + total_process_count = cppprocesses + rustprocesses + + config = { + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/opt", + "ready_timeout": 2.0, + "shutdown_timeout": 2.0, + "ready_recovery_action": {"restart": {"number_of_attempts": 0}}, + "recovery_action": {"switch_run_target": {"run_target": "Off"}}, + "environmental_variables": {"LD_LIBRARY_PATH": "/opt/lib"}, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + }, + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": False, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1, + }, + }, + "ready_condition": {"process_state": "Running"}, + }, + }, + "components": {}, + "run_targets": {}, + "initial_run_target": "Off", + "alive_supervision": {"evaluation_cycle": 0.05}, + } + + running_deps = [] + + # --- Control daemon --- + config["components"]["control_daemon"] = { + "component_properties": { + "binary_name": "control_app/control_daemon", + "application_profile": { + "application_type": "State_Manager", + }, + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": {"PROCESSIDENTIFIER": "control_daemon"}, + }, + } + running_deps.append("control_daemon") + + # --- Supervised demo processes --- + for i in range(total_process_count): + if is_rust_app(i, cppprocesses, rustprocesses): + binary = "supervision_demo/rust_supervised_app" + print(f"Rust Process with index {i}") + else: + binary = "supervision_demo/cpp_supervised_app" + print(f"CPP Process with index {i}") + + comp_name = f"demo_app{i}" + config["components"][comp_name] = { + "component_properties": { + "binary_name": binary, + "application_profile": { + "application_type": "Reporting_And_Supervised", + }, + "process_arguments": ["-d50"], + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": f"MainPG_app{i}", + "CONFIG_PATH": f"/opt/supervision_demo/etc/{comp_name}.bin", + "IDENTIFIER": comp_name, + }, + }, + } + running_deps.append(comp_name) + + # --- Non-supervised lifecycle processes --- + for i in range(non_supervised_processes): + comp_name = f"MainPG_lifecycle_app{i}" + config["components"][comp_name] = { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app", + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": f"MainPG_lc{i}", + }, + }, + } + running_deps.append(comp_name) + + # --- Fallback verbose lifecycle app (runs during recovery) --- + config["components"]["fallback_app"] = { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app", + "process_arguments": ["-v"], + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "fallback_app", + }, + }, + } + + # --- Run targets --- + config["run_targets"]["Off"] = { + "depends_on": [], + "recovery_action": {"switch_run_target": {"run_target": "Off"}}, + } + + config["run_targets"]["Startup"] = { + "depends_on": ["control_daemon"], + "recovery_action": {"switch_run_target": {"run_target": "Off"}}, + } + + config["run_targets"]["Running"] = { + "depends_on": running_deps, + "recovery_action": {"switch_run_target": {"run_target": "Off"}}, + } + + # Fallback run target: control daemon + verbose app run during recovery + config["fallback_run_target"] = {"depends_on": ["control_daemon", "fallback_app"]} + + return config + + +if __name__ == "__main__": + my_parser = argparse.ArgumentParser( + description="Generate unified lifecycle configuration in the S-CORE Launch Manager schema format." + ) + my_parser.add_argument( + "-c", + "--cppprocesses", + action="store", + type=int, + required=True, + help="Number of C++ supervised processes per process group", + ) + my_parser.add_argument( + "-r", + "--rustprocesses", + action="store", + type=int, + required=True, + help="Number of Rust supervised processes per process group", + ) + my_parser.add_argument( + "-n", + "--non-supervised-processes", + action="store", + type=int, + required=True, + help="Number of C++ non-supervised (lifecycle) processes per process group", + ) + my_parser.add_argument( + "-o", + "--out", + action="store", + type=Path, + required=True, + help="Output directory", + ) + args = my_parser.parse_args() + + if args.cppprocesses < 0 or args.cppprocesses > 10000: + print("Number of C++ processes must be between 0 and 10000") + exit(1) + if args.rustprocesses < 0 or args.rustprocesses > 10000: + print("Number of Rust processes must be between 0 and 10000") + exit(1) + if args.non_supervised_processes < 0 or args.non_supervised_processes > 10000: + print("Number of non-supervised processes must be between 0 and 10000") + exit(1) + + cfg = gen_lifecycle_config( + args.cppprocesses, + args.rustprocesses, + args.non_supervised_processes, + ) + + cfg_out_path = os.path.join(args.out, "lifecycle_demo.json") + with open(cfg_out_path, "w") as f: + json.dump(cfg, f, indent=4) + + print(f"Generated lifecycle configuration: {cfg_out_path}") diff --git a/examples/demo.sh b/examples/demo.sh index 18213fb7..69f167be 100755 --- a/examples/demo.sh +++ b/examples/demo.sh @@ -20,9 +20,9 @@ ps -a | head -n 40 echo "$> ps -a | wc -l" ps -a | wc -l -read -p "$(echo -e ${COLOR}Next: Turning on ProcessGroup1/Startup${NC})" -echo "$> lmcontrol ProcessGroup1/Startup" -lmcontrol ProcessGroup1/Startup +read -p "$(echo -e ${COLOR}Next: Turning on MainPG/Running${NC})" +echo "$> lmcontrol MainPG/Running" +lmcontrol MainPG/Running read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a | wc -l" @@ -32,15 +32,18 @@ read -p "$(echo -e ${COLOR}Next: Show CPU utilization${NC})" echo "$> top" top -read -p "$(echo -e ${COLOR}Next: Turning off ProcessGroup1/Startup${NC})" -echo "$> lmcontrol ProcessGroup1/Off" -lmcontrol ProcessGroup1/Off +read -p "$(echo -e ${COLOR}Next: Turning off demo apps via MainPG/Startup${NC})" +echo "$> lmcontrol MainPG/Startup" +lmcontrol MainPG/Startup read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a | wc -l" ps -a | wc -l read -p "$(echo -e ${COLOR}Next: Killing an application process${NC})" +echo "$> lmcontrol MainPG/Running" +lmcontrol MainPG/Running +sleep 2 echo "$> pkill -9 MainPG_lc0" pkill -9 MainPG_lc0 read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" @@ -49,9 +52,9 @@ ps -a echo "$> ps -a | wc -l" ps -a | wc -l -read -p "$(echo -e ${COLOR}Next: Moving back to MainPG/Startup${NC})" -echo "$> lmcontrol MainPG/Startup" -lmcontrol MainPG/Startup +read -p "$(echo -e ${COLOR}Next: Moving back to MainPG/Running${NC})" +echo "$> lmcontrol MainPG/Running" +lmcontrol MainPG/Running read -p "$(echo -e ${COLOR}Next: Trigger supervision failure${NC})" echo "$> fail $(pgrep MainPG_app0)" diff --git a/examples/run.sh b/examples/run.sh index dd704251..6b51615e 100755 --- a/examples/run.sh +++ b/examples/run.sh @@ -37,24 +37,28 @@ file_exists $RUST_APP_BINARY file_exists $CONTROL_APP_BINARY file_exists $CONTROL_CLI_BINARY -NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP=1 -NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP=1 -NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES_PER_PROCESS_GROUP=1 -PROCESS_GROUPS="--process_groups MainPG ProcessGroup1" +NUMBER_OF_CPP_PROCESSES=1 +NUMBER_OF_RUST_PROCESSES=1 +NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES=1 rm -rf tmp rm -rf config/tmp mkdir config/tmp -python3 config/gen_health_monitor_process_cfg.py -c "$NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP" -r "$NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP" $PROCESS_GROUPS -o config/tmp/ -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs config/tmp/health_monitor_process_cfg_*.json -python3 config/gen_health_monitor_cfg.py -c "$NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP" -r "$NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP" $PROCESS_GROUPS -o config/tmp/ -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs config/tmp/hm_demo.json +python3 config/gen_lifecycle_config.py -c "$NUMBER_OF_CPP_PROCESSES" -r "$NUMBER_OF_RUST_PROCESSES" -n "$NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES" -o config/tmp/ + +python3 ../scripts/config_mapping/lifecycle_config.py config/tmp/lifecycle_demo.json -o config/tmp/ + +for f in config/tmp/*.json; do + base=$(basename "$f") + if [[ "$base" != "lm_demo.json" && "$base" != "hmcore.json" && "$base" != "lifecycle_demo.json" ]]; then + ../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs "$f" + fi +done -python3 config/gen_launch_manager_cfg.py -c "$NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP" -r "$NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP" -n "$NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES_PER_PROCESS_GROUP" $PROCESS_GROUPS -o config/tmp/ ../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/config/lm_flatcfg.fbs config/tmp/lm_demo.json -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hmcore_flatcfg.fbs config/hmcore.json +../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hmcore_flatcfg.fbs config/tmp/hmcore.json mkdir -p tmp/launch_manager/etc cp $LM_BINARY tmp/launch_manager/launch_manager @@ -66,13 +70,12 @@ cp config/tmp/hmcore.bin tmp/launch_manager/etc/ mkdir -p tmp/supervision_demo/etc cp $DEMO_APP_BINARY tmp/supervision_demo/ -cp config/tmp/health_monitor_process_cfg_*.bin tmp/supervision_demo/etc/ +cp config/tmp/demo_app*.bin tmp/supervision_demo/etc/ mkdir -p tmp/cpp_lifecycle_app/etc cp $DEMO_APP_WO_HM_BINARY tmp/cpp_lifecycle_app/ cp $RUST_APP_BINARY tmp/supervision_demo/ -cp config/tmp/health_monitor_process_cfg_*.bin tmp/supervision_demo/etc/ mkdir -p tmp/control_app cp $CONTROL_APP_BINARY tmp/control_app/ diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index a9e7beed..76cfc3e2 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -663,7 +663,7 @@ def get_process_type(application_type): return "REGULAR_PROCESS" def is_supervised(application_type): - return application_type == "State_Manager" or application_type == "Reporting_And_Supervised" + return application_type == "Reporting_And_Supervised" def get_all_process_group_states(run_targets): process_group_states = [] @@ -692,15 +692,43 @@ def get_all_refProcessGroupStates(run_targets): hm_config["hmLocalSupervision"] = [] hm_config["hmGlobalSupervision"] = [] hm_config["hmRecoveryNotification"] = [] + # Build a mapping of run_target -> list of supervised component names + run_target_components = {} + for rt_name, rt_config in config["run_targets"].items(): + if rt_name == "Off": + continue + supervised_deps = [] + for dep_name in rt_config.get("depends_on", []): + if dep_name in config["components"]: + comp = config["components"][dep_name] + if is_supervised(comp["component_properties"]["application_profile"]["application_type"]): + supervised_deps.append(dep_name) + if supervised_deps: + run_target_components[rt_name] = supervised_deps + index = 0 + # Track which process indices belong to each run target + run_target_indices = {rt: [] for rt in run_target_components} + for component_name, component_config in config["components"].items(): if is_supervised(component_config["component_properties"]["application_profile"]["application_type"]): + # Find which run target this component belongs to + component_rt = None + for rt_name, comp_list in run_target_components.items(): + if component_name in comp_list: + component_rt = rt_name + break + process = {} process["index"] = index process["shortName"] = component_name process["identifier"] = component_name process["processType"] = get_process_type(component_config["component_properties"]["application_profile"]["application_type"]) - process["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + # Each process references only its own run target's process group state + if component_rt: + process["refProcessGroupStates"] = [{"identifier": "MainPG/" + component_rt}] + else: + process["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) process["processExecutionErrors"] = [{"processExecutionError":1}] hm_config["process"].append(process) @@ -729,7 +757,10 @@ def get_all_refProcessGroupStates(run_targets): alive_supervision["isMaxCheckDisabled"] = alive_supervision["maxAliveIndications"] == 0 alive_supervision["failedSupervisionCyclesTolerance"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["failed_cycles_tolerance"] alive_supervision["refProcessIndex"] = index - alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + if component_rt: + alive_supervision["refProcessGroupStates"] = [{"identifier": "MainPG/" + component_rt}] + else: + alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) hm_config["hmAliveSupervision"].append(alive_supervision) local_supervision = {} @@ -747,23 +778,31 @@ def get_all_refProcessGroupStates(run_targets): process_config["hmMonitorInterface"].append(hmMonitorIf) json.dump(process_config, process_file, indent=4) + if component_rt: + run_target_indices[component_rt].append(index) index += 1 - indices = [i for i in range(index)] - if len(indices) > 0: - # Create one global supervision & recovery action for all processes. + # Create one global supervision & recovery notification per run target (process group) + recovery_state = get_recovery_process_group_state(config) + for rt_name, rt_indices in run_target_components.items(): + proc_indices = run_target_indices.get(rt_name, []) + if not proc_indices: + continue + global_supervision = {} - global_supervision["ruleContextKey"] = "global_supervision" + global_supervision["ruleContextKey"] = f"GlobalSupervision_{rt_name}" global_supervision["isSeverityCritical"] = False - global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in indices] - global_supervision["refProcesses"] = [{"index": idx} for idx in indices] - global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in proc_indices] + global_supervision["refProcesses"] = [{"index": idx} for idx in proc_indices] + global_supervision["refProcessGroupStates"] = [{"identifier": "MainPG/" + rt_name}] + gs_index = len(hm_config["hmGlobalSupervision"]) hm_config["hmGlobalSupervision"].append(global_supervision) recovery_action = {} + recovery_action["shortName"] = f"RecoveryNotification_{rt_name}" recovery_action["recoveryNotificationTimeout"] = 5000 - recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) - recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(global_supervision) + recovery_action["processGroupMetaModelIdentifier"] = recovery_state + recovery_action["refGlobalSupervisionIndex"] = gs_index recovery_action["instanceSpecifier"] = "" recovery_action["shouldFireWatchdog"] = False hm_config["hmRecoveryNotification"].append(recovery_action) From ddd00a4943bfc39ac2b4bfea7479f6c41b1f53d8 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 08:35:05 +0100 Subject: [PATCH 27/72] Cleanup --- examples/config/gen_lifecycle_config.py | 22 +++++++--------------- examples/demo.sh | 8 ++++---- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/examples/config/gen_lifecycle_config.py b/examples/config/gen_lifecycle_config.py index 2b824fdd..419858cf 100644 --- a/examples/config/gen_lifecycle_config.py +++ b/examples/config/gen_lifecycle_config.py @@ -25,12 +25,6 @@ from pathlib import Path -def is_rust_app(process_index: int, cppprocess_count: int, rustprocess_count: int): - processes_per_process_group = cppprocess_count + rustprocess_count - index_in_group = process_index % processes_per_process_group - return index_in_group >= cppprocess_count - - def gen_lifecycle_config( cppprocesses: int, rustprocesses: int, @@ -95,12 +89,10 @@ def gen_lifecycle_config( # --- Supervised demo processes --- for i in range(total_process_count): - if is_rust_app(i, cppprocesses, rustprocesses): + if i >= cppprocesses: binary = "supervision_demo/rust_supervised_app" - print(f"Rust Process with index {i}") else: binary = "supervision_demo/cpp_supervised_app" - print(f"CPP Process with index {i}") comp_name = f"demo_app{i}" config["components"][comp_name] = { @@ -113,7 +105,7 @@ def gen_lifecycle_config( }, "deployment_config": { "environmental_variables": { - "PROCESSIDENTIFIER": f"MainPG_app{i}", + "PROCESSIDENTIFIER": comp_name, "CONFIG_PATH": f"/opt/supervision_demo/etc/{comp_name}.bin", "IDENTIFIER": comp_name, }, @@ -123,14 +115,14 @@ def gen_lifecycle_config( # --- Non-supervised lifecycle processes --- for i in range(non_supervised_processes): - comp_name = f"MainPG_lifecycle_app{i}" + comp_name = f"lifecycle_app{i}" config["components"][comp_name] = { "component_properties": { "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app", }, "deployment_config": { "environmental_variables": { - "PROCESSIDENTIFIER": f"MainPG_lc{i}", + "PROCESSIDENTIFIER": comp_name, }, }, } @@ -181,7 +173,7 @@ def gen_lifecycle_config( action="store", type=int, required=True, - help="Number of C++ supervised processes per process group", + help="Number of C++ supervised processes", ) my_parser.add_argument( "-r", @@ -189,7 +181,7 @@ def gen_lifecycle_config( action="store", type=int, required=True, - help="Number of Rust supervised processes per process group", + help="Number of Rust supervised processes", ) my_parser.add_argument( "-n", @@ -197,7 +189,7 @@ def gen_lifecycle_config( action="store", type=int, required=True, - help="Number of C++ non-supervised (lifecycle) processes per process group", + help="Number of C++ non-supervised (lifecycle) processes", ) my_parser.add_argument( "-o", diff --git a/examples/demo.sh b/examples/demo.sh index 69f167be..ed4d4605 100755 --- a/examples/demo.sh +++ b/examples/demo.sh @@ -44,8 +44,8 @@ read -p "$(echo -e ${COLOR}Next: Killing an application process${NC})" echo "$> lmcontrol MainPG/Running" lmcontrol MainPG/Running sleep 2 -echo "$> pkill -9 MainPG_lc0" -pkill -9 MainPG_lc0 +echo "$> pkill -9 demo_app0" +pkill -9 demo_app0 read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a" ps -a @@ -57,8 +57,8 @@ echo "$> lmcontrol MainPG/Running" lmcontrol MainPG/Running read -p "$(echo -e ${COLOR}Next: Trigger supervision failure${NC})" -echo "$> fail $(pgrep MainPG_app0)" -kill -s SIGUSR1 $(pgrep MainPG_app0) +echo "$> fail $(pgrep demo_app0)" +kill -s SIGUSR1 $(pgrep demo_app0) read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a" From 3c8994096c1b58ba25c06931be7b9833e065614e Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 09:44:00 +0100 Subject: [PATCH 28/72] Undo some changes --- scripts/config_mapping/lifecycle_config.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 76cfc3e2..780d7b8f 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -782,27 +782,23 @@ def get_all_refProcessGroupStates(run_targets): run_target_indices[component_rt].append(index) index += 1 - # Create one global supervision & recovery notification per run target (process group) - recovery_state = get_recovery_process_group_state(config) - for rt_name, rt_indices in run_target_components.items(): - proc_indices = run_target_indices.get(rt_name, []) - if not proc_indices: - continue - + indices = [i for i in range(index)] + if len(indices) > 0: + # Create one global supervision & recovery action for all processes. global_supervision = {} - global_supervision["ruleContextKey"] = f"GlobalSupervision_{rt_name}" + global_supervision["ruleContextKey"] = "global_supervision" global_supervision["isSeverityCritical"] = False - global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in proc_indices] - global_supervision["refProcesses"] = [{"index": idx} for idx in proc_indices] - global_supervision["refProcessGroupStates"] = [{"identifier": "MainPG/" + rt_name}] + global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in indices] + global_supervision["refProcesses"] = [{"index": idx} for idx in indices] + global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) gs_index = len(hm_config["hmGlobalSupervision"]) hm_config["hmGlobalSupervision"].append(global_supervision) recovery_action = {} recovery_action["shortName"] = f"RecoveryNotification_{rt_name}" recovery_action["recoveryNotificationTimeout"] = 5000 - recovery_action["processGroupMetaModelIdentifier"] = recovery_state - recovery_action["refGlobalSupervisionIndex"] = gs_index + recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) + recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(global_supervision) recovery_action["instanceSpecifier"] = "" recovery_action["shouldFireWatchdog"] = False hm_config["hmRecoveryNotification"].append(recovery_action) From ac5126486e2bf65227f6f4b6108e682ad1143bb1 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 09:46:27 +0100 Subject: [PATCH 29/72] Undo some changes --- scripts/config_mapping/lifecycle_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 780d7b8f..0570c751 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -784,14 +784,13 @@ def get_all_refProcessGroupStates(run_targets): indices = [i for i in range(index)] if len(indices) > 0: - # Create one global supervision & recovery action for all processes. + # Create one global supervision & recovery action for all processes. global_supervision = {} global_supervision["ruleContextKey"] = "global_supervision" global_supervision["isSeverityCritical"] = False global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in indices] global_supervision["refProcesses"] = [{"index": idx} for idx in indices] global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) - gs_index = len(hm_config["hmGlobalSupervision"]) hm_config["hmGlobalSupervision"].append(global_supervision) recovery_action = {} From 1fb95cdcd66409ec9cde3dc379f745930bd66ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 20 Feb 2026 17:36:49 +0100 Subject: [PATCH 30/72] Bazel command for lifecylce config gen --- scripts/config_mapping/BUILD | 31 + scripts/config_mapping/config.bzl | 184 ++++++ scripts/config_mapping/flatbuffer_rules.bzl | 77 +++ scripts/config_mapping/lifecycle_config.py | 542 +----------------- src/launch_manager_daemon/BUILD | 4 +- src/launch_manager_daemon/config/BUILD | 5 + .../config/s-core_launch_manager.schema.json | 498 ++++++++++++++++ 7 files changed, 827 insertions(+), 514 deletions(-) create mode 100644 scripts/config_mapping/BUILD create mode 100644 scripts/config_mapping/config.bzl create mode 100644 scripts/config_mapping/flatbuffer_rules.bzl create mode 100644 src/launch_manager_daemon/config/BUILD create mode 100644 src/launch_manager_daemon/config/s-core_launch_manager.schema.json diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD new file mode 100644 index 00000000..565042f2 --- /dev/null +++ b/scripts/config_mapping/BUILD @@ -0,0 +1,31 @@ + +load("//scripts/config_mapping:config.bzl", "lifecycle_config") +load("//scripts/config_mapping:config.bzl", "lifecycle_config_action") + +exports_files(["lm_config.json"]) + +#sh_binary( +# name = "lifecycle_config", +# srcs = ["lifecycle_config_wrapper.sh"], +# data = [ +# "lifecycle_config.py", +# "requirements.txt", +# ], +# visibility = ["//visibility:public"], +#) + + +py_binary( + name = "lifecycle_config", + srcs = ["lifecycle_config.py"], +) + +#lifecycle_config(config_path="//scripts/config_mapping:lm_config.json", +# output_files=["hm_demo.json", "lm_demo.json", "hmcore.json", "someip-daemon.json", +# "state_manager.json", "test_app1.json"]) +# + +lifecycle_config_action( + name ="lifecycle_config_action", + config="//scripts/config_mapping:lm_config.json" +) diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl new file mode 100644 index 00000000..56f91d29 --- /dev/null +++ b/scripts/config_mapping/config.bzl @@ -0,0 +1,184 @@ +#load("//scripts/config_mapping/flatbuffers_rules.bzl", "flatbuffer_json_to_bin") + +def lifecycle_config(config_path, output_files=[]): + + native.genrule( + name = "gen_lifecycle_config", + srcs = [config_path, "//src/launch_manager_daemon/config:s-core_launch_manager.schema.json"], + outs = output_files, + cmd = """ + python3 $(location //scripts/config_mapping:lifecycle_config.py) $(location {input_json}) \ + --schema $(location //src/launch_manager_daemon/config:s-core_launch_manager.schema.json) -o $(@D) + """.format(input_json=config_path), + + tools = ["//scripts/config_mapping:lifecycle_config.py"], + visibility = ["//visibility:public"], + ) + +#def _flatbuffer_json_to_bin_impl_tmp(ctx): +# flatc = ctx.executable.flatc +# json = ctx.file.json +# schema = ctx.file.schema +# +# # flatc will name the file the same as the json (can't be changed) +# out_name = json.basename[:-len(".json")] + ".bin" +# out = ctx.actions.declare_file(out_name, sibling = json) +# +# # flatc args --------------------------------- +# flatc_args = [ +# "-b", +# "-o", +# out.dirname, +# ] +# +# for inc in ctx.attr.includes: +# flatc_args.extend(["-I", inc.path]) +# +# if ctx.attr.strict_json: +# flatc_args.append("--strict-json") +# +# flatc_args.extend([schema.path, json.path]) +# # -------------------------------------------- +# +# ctx.actions.run( +# inputs = [json, schema] + list(ctx.files.includes), +# outputs = [out], +# executable = flatc, +# arguments = flatc_args, +# progress_message = "flatc generation {}".format(json.short_path), +# mnemonic = "FlatcGeneration", +# ) +# +# rf = ctx.runfiles( +# files = [out], +# root_symlinks = { +# ("_main/" + ctx.attr.out_dir + "/" + out_name): out, +# }, +# ) + +def _lifecycle_config_impl(ctx): + config = ctx.file.config + schema = ctx.file.schema + script = ctx.executable.script + json_out_dir = ctx.attr.json_out_dir + + # Get Python runtime + python_runtime = ctx.toolchains["@rules_python//python:toolchain_type"].py3_runtime + python_exe = python_runtime.interpreter + + # First run_shell - creates directory with files inside + gen_dir_json = ctx.actions.declare_directory(json_out_dir) + ctx.actions.run_shell( + inputs = [config, schema], + outputs = [gen_dir_json], + tools = [script, python_exe], + command = """ + export PYTHON3={} + export PATH="$(dirname {}):$PATH" + {} {} --schema {} -o {} + """.format(python_exe.path, python_exe.path, script.path, config.path, schema.path, gen_dir_json.path), + arguments = [] + ) + + flatbuffer_out_dir = ctx.attr.flatbuffer_out_dir + flatc = ctx.executable.flatc + lm_schema = ctx.file.lm_schema + hm_schema = ctx.file.hm_schema + hmcore_schema = ctx.file.hmcore_schema + + # Second run_shell - processes the files from the generated directory + gen_dir_flatbuffer = ctx.actions.declare_directory(flatbuffer_out_dir) + ctx.actions.run_shell( + inputs = [gen_dir_json, lm_schema, hm_schema, hmcore_schema], + outputs = [gen_dir_flatbuffer], + tools = [flatc], + command = """ + mkdir -p {gen_dir_flatbuffer} + # Process each file from generated directory + for file in {gen_dir_json}/*; do + if [ -f "$file" ]; then + filename=$(basename "$file") + + if [[ "$filename" == "lm_"* ]]; then + schema={lm_schema} + elif [[ "$filename" == "hmcore"* ]]; then + schema={hmcore_schema} + elif [[ "$filename" == "hm_"* ]]; then + schema={hm_schema} + elif [[ "$filename" == "hmproc_"* ]]; then + schema={hm_schema} + else + echo "Unknown file type for $filename, skipping." + continue + fi + + # Process with flatc + {flatc} -b -o {gen_dir_flatbuffer} "$schema" "$file" + fi + done + """.format( + gen_dir_flatbuffer=gen_dir_flatbuffer.path, + gen_dir_json=gen_dir_json.path, + lm_schema=lm_schema.path, + hmcore_schema=hmcore_schema.path, + hm_schema=hm_schema.path, + flatc=flatc.path + ), + arguments = [] + ) + + return DefaultInfo(files = depset([gen_dir_json, gen_dir_flatbuffer])) + + +lifecycle_config_action = rule( + implementation = _lifecycle_config_impl, + attrs = { + "config": attr.label( + allow_single_file = [".json"], + mandatory = True, + doc = "Json file to convert. Note that the binary file will have the same name as the json (minus the suffix)", + ), + "schema": attr.label( + default=Label("//src/launch_manager_daemon/config:s-core_launch_manager.schema.json"), + allow_single_file = [".json"], + doc = "Json schema file to validate the input json against", + ), + "script": attr.label( + default = Label("//scripts/config_mapping:lifecycle_config"), + executable = True, + cfg = "exec", + doc = "Python script to execute", + ), + "json_out_dir": attr.string( + default = "json_out", + doc = "Directory to copy the generated file to. Do not include a trailing '/'", + ), + "flatbuffer_out_dir": attr.string( + default = "flatbuffer_out", + doc = "Directory to copy the generated file to. Do not include a trailing '/'", + ), + "flatc": attr.label( + default = Label("@flatbuffers//:flatc"), + executable = True, + cfg = "exec", + doc = "Reference to the flatc binary", + ), + "lm_schema": attr.label( + allow_single_file = [".fbs"], + default = Label("//src/launch_manager_daemon:lm_flatcfg_fbs"), + doc = "Launch Manager fbs file to use", + ), + "hm_schema": attr.label( + allow_single_file = [".fbs"], + default=Label("//src/launch_manager_daemon/health_monitor_lib:hm_flatcfg_fbs"), + doc = "HealthMonitor fbs file to use", + ), + "hmcore_schema": attr.label( + allow_single_file = [".fbs"], + default=Label("//src/launch_manager_daemon/health_monitor_lib:hmcore_flatcfg_fbs"), + doc = "HealthMonitor core fbs file to use", + ) + }, + toolchains = ["@rules_python//python:toolchain_type"], +) + diff --git a/scripts/config_mapping/flatbuffer_rules.bzl b/scripts/config_mapping/flatbuffer_rules.bzl new file mode 100644 index 00000000..366d96ef --- /dev/null +++ b/scripts/config_mapping/flatbuffer_rules.bzl @@ -0,0 +1,77 @@ +def _flatbuffer_json_to_bin_impl(ctx): + flatc = ctx.executable.flatc + json = ctx.file.json + schema = ctx.file.schema + + # flatc will name the file the same as the json (can't be changed) + out_name = json.basename[:-len(".json")] + ".bin" + out = ctx.actions.declare_file(out_name, sibling = json) + + # flatc args --------------------------------- + flatc_args = [ + "-b", + "-o", + out.dirname, + ] + + for inc in ctx.attr.includes: + flatc_args.extend(["-I", inc.path]) + + if ctx.attr.strict_json: + flatc_args.append("--strict-json") + + flatc_args.extend([schema.path, json.path]) + # -------------------------------------------- + + ctx.actions.run( + inputs = [json, schema] + list(ctx.files.includes), + outputs = [out], + executable = flatc, + arguments = flatc_args, + progress_message = "flatc generation {}".format(json.short_path), + mnemonic = "FlatcGeneration", + ) + + rf = ctx.runfiles( + files = [out], + root_symlinks = { + ("_main/" + ctx.attr.out_dir + "/" + out_name): out, + }, + ) + + return DefaultInfo(files = depset([out]), runfiles = rf) + +flatbuffer_json_to_bin = rule( + implementation = _flatbuffer_json_to_bin_impl, + attrs = { + "json": attr.label( + allow_single_file = [".json"], + mandatory = True, + doc = "Json file to convert. Note that the binary file will have the same name as the json (minus the suffix)", + ), + "schema": attr.label( + allow_single_file = [".fbs"], + mandatory = True, + doc = "FBS file to use", + ), + "out_dir": attr.string( + default = "etc", + doc = "Directory to copy the generated file to, sibling to 'src' and 'tests' dirs. Do not include a trailing '/'", + ), + "flatc": attr.label( + default = Label("@flatbuffers//:flatc"), + executable = True, + cfg = "exec", + doc = "Reference to the flatc binary", + ), + # flatc arguments + "includes": attr.label_list( + allow_files = True, + doc = "Flatc include paths", + ), + "strict_json": attr.bool( + default = False, + doc = "Require strict JSON (no trailing commas etc)", + ), + }, +) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 0d30997e..7412a23e 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -6,508 +6,6 @@ import sys from typing import Dict, Any -# TODO -json_schema = json.loads(""" -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "s-core_launch_manager.schema.json", - "title": "Configuration schema for the S-CORE Launch Manager", - "type": "object", - "$defs": { - "component_properties": { - "type": "object", - "description": "Defines a reusable type that captures the essential development-time characteristics of a software component.", - "properties": { - "binary_name": { - "type": "string", - "description": "Specifies the relative path of the executable file inside the directory defined by 'deployment_config.bin_dir'. The final executable path will be resolved as '{bin_dir}/{binary_name}'. Example values include simple filenames (e.g., 'test_app1') or subdirectory paths (e.g., 'bin/test_app1')." - }, - "application_profile": { - "type": "object", - "description": "Specifies the application profile that defines the runtime behavior and capabilities of this component.", - "properties": { - "application_type": { - "type": "string", - "enum": [ - "Native", - "Reporting", - "Reporting_And_Supervised", - "State_Manager" - ], - "description": "Specifies the level of integration between the component and the Launch Manager. 'Native': no integration with Launch Manager. 'Reporting': uses Launch Manager lifecycle APIs. 'Reporting_And_Supervised': uses lifecycle APIs and sends alive notifications. 'State_Manager': uses lifecycle APIs, sends alive notifications, and has permission to change the active Run Target." - }, - "is_self_terminating": { - "type": "boolean", - "description": "Indicates whether component is designed to terminate automatically once its planned tasks are completed (true), or remain running until explicitly requested to terminate by the Launch Manager (false)." - }, - "alive_supervision": { - "type": "object", - "description": "Specifies the configuration parameters used for alive monitoring of the component.", - "properties": { - "reporting_cycle": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the duration of the time interval used to verify that the component sends alive notifications, within the expected time frame." - }, - "failed_cycles_tolerance": { - "type": "integer", - "minimum": 0, - "description": "Defines the maximum number of consecutive reporting cycle failures (see 'reporting_cycle'). Once the number of failed cycles goes above maximum number, Launch Manager will trigger configured recovery action." - }, - "min_indications": { - "type": "integer", - "minimum": 0, - "description": "Specifies the minimum number of checkpoints that must be reported within each configured 'reporting_cycle'." - }, - "max_indications": { - "type": "integer", - "minimum": 0, - "description": "Specifies the maximum number of checkpoints that may be reported within each configured 'reporting_cycle'." - } - }, - "required": [], - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "depends_on": { - "type": "array", - "description": "Names of components that this component depends on. Each dependency must be initialized and reach its ready state before this component can start.", - "items": { - "type": "string", - "description": "Specifies the name of a component on which this component depends." - } - }, - "process_arguments": { - "type": "array", - "description": "Ordered list of command-line arguments passed to the component at startup.", - "items": { - "type": "string", - "description": "Single command-line argument token as a UTF-8 string; order is preserved." - } - }, - "ready_condition": { - "type": "object", - "description": "Specifies the set of conditions that mark when the component completes its initializing state and enters the ready state.", - "properties": { - "process_state": { - "type": "string", - "enum": [ - "Running", - "Terminated" - ], - "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." - } - }, - "required": [], - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "recovery_action": { - "type": "object", - "description": "Defines a reusable type that specifies which recovery actions should be executed when an error or failure occurs.", - "properties": { - "restart": { - "type": "object", - "description": "Specifies a recovery action that restarts the POSIX process associated with this component.", - "properties": { - "number_of_attempts": { - "type": "integer", - "minimum": 0, - "description": "Specifies the maximum number of restart attempts before the Launch Manager concludes that recovery cannot succeed." - }, - "delay_before_restart": { - "type": "number", - "minimum": 0, - "description": "Specifies the delay duration that Launch Manager shall wait before initiating a restart attempt." - } - }, - "required": [], - "additionalProperties": false - }, - "switch_run_target": { - "type": "object", - "description": "Specifies a recovery action that switches to a Run Target. This can be a different Run Target or the same one to retry activation of the current Run Target.", - "properties": { - "run_target": { - "type": "string", - "description": "Specifies the name of the Run Target that Launch Manager should switch to." - } - }, - "required": [], - "additionalProperties": false - } - }, - "oneOf": [ - { - "required": [ - "restart" - ] - }, - { - "required": [ - "switch_run_target" - ] - } - ], - "additionalProperties": false - }, - "deployment_config": { - "type": "object", - "description": "Defines a reusable type that contains the configuration parameters that are specific to a particular deployment environment or system setup.", - "properties": { - "ready_timeout": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the maximum time allowed for the component to reach its ready state. The timeout is measured from when the component process is created until the ready conditions specified in 'component_properties.ready_condition' are met." - }, - "shutdown_timeout": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the maximum allowed time for the component to terminate after it receives a SIGTERM signal from the Launch Manager. The timeout is measured from the moment the Launch Manager sends the SIGTERM signal, until the Operating System notifies the Launch Manager that the child process has terminated." - }, - "environmental_variables": { - "type": "object", - "description": "Defines the set of environment variables that will be passed to the component at startup.", - "additionalProperties": { - "type": "string", - "description": "Specifies the environment variable's value as a string. An empty string is allowed and represents an intentionally empty environment variable." - } - }, - "bin_dir": { - "type": "string", - "description": "Specifies the absolute filesystem path to the directory where component is installed." - }, - "working_dir": { - "type": "string", - "description": "Specifies the directory that will be used as the working directory for the component during execution." - }, - "ready_recovery_action": { - "allOf": [ - { - "$ref": "#/$defs/recovery_action" - }, - { - "properties": { - "restart": true - }, - "required": [ - "restart" - ], - "not": { - "required": [ - "switch_run_target" - ] - } - } - ], - "description": "Specifies the recovery action to execute when the component fails to reach its ready state within the configured timeout." - }, - "recovery_action": { - "allOf": [ - { - "$ref": "#/$defs/recovery_action" - }, - { - "properties": { - "switch_run_target": true - }, - "required": [ - "switch_run_target" - ], - "not": { - "required": [ - "restart" - ] - } - } - ], - "description": "Specifies the recovery action to execute when the component malfunctions after reaching its ready state." - }, - "sandbox": { - "type": "object", - "description": "Specifies the sandbox configuration parameters that isolate and constrain the component's runtime execution.", - "properties": { - "uid": { - "type": "integer", - "minimum": 0, - "description": "Specifies the POSIX user ID (UID) under which this component executes." - }, - "gid": { - "type": "integer", - "minimum": 0, - "description": "Specifies the primary POSIX group ID (GID) under which this component executes." - }, - "supplementary_group_ids": { - "type": "array", - "description": "Specifies the list of supplementary POSIX group IDs (GIDs) assigned to this component.", - "items": { - "type": "integer", - "minimum": 0, - "description": "Single supplementary POSIX group ID (GID)" - } - }, - "security_policy": { - "type": "string", - "description": "Specifies the security policy or confinement profile name (such as an SELinux or AppArmor profile) assigned to the component." - }, - "scheduling_policy": { - "type": "string", - "description": "Specifies the scheduling policy applied to the component's initial thread. Supported values correspond to OS-defined policies (e.g., FIFO, RR, OTHER).", - "anyOf": [ - { - "enum": [ - "SCHED_FIFO", - "SCHED_RR", - "SCHED_OTHER" - ] - }, - { - "type": "string" - } - ] - }, - "scheduling_priority": { - "type": "integer", - "description": "Specifies the scheduling priority applied to the component's initial thread." - }, - "max_memory_usage": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "Specifies the maximum amount of memory, in bytes, that the component is permitted to use during runtime." - }, - "max_cpu_usage": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage of total CPU capacity." - } - }, - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "run_target": { - "type": "object", - "description": "Defines a reusable type that specifies the configuration parameters for a Run Target.", - "properties": { - "description": { - "type": "string", - "description": "User-defined description of the configured Run Target." - }, - "depends_on": { - "type": "array", - "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", - "items": { - "type": "string", - "description": "Name of a component or Run Target that this Run Target depends on." - } - }, - "transition_timeout": { - "type": "number", - "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", - "exclusiveMinimum": 0 - }, - "recovery_action": { - "allOf": [ - { - "$ref": "#/$defs/recovery_action" - }, - { - "properties": { - "switch_run_target": true - }, - "required": [ - "switch_run_target" - ], - "not": { - "required": [ - "restart" - ] - } - } - ], - "description": "Specifies the recovery action to execute when a component assigned to this Run Target fails." - } - }, - "required": [ - "recovery_action" - ], - "additionalProperties": false - }, - "alive_supervision": { - "type": "object", - "description": "Defines a reusable type that contains the configuration parameters for alive supervision", - "properties": { - "evaluation_cycle": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the length of the time window used to assess incoming alive supervision reports." - } - }, - "required": [], - "additionalProperties": false - }, - "watchdog": { - "type": "object", - "description": "Defines a reusable type that contains the configuration parameters for the external watchdog.", - "properties": { - "device_file_path": { - "type": "string", - "description": "Path to the external watchdog device file (e.g., /dev/watchdog)." - }, - "max_timeout": { - "type": "number", - "minimum": 0, - "description": "Specifies the maximum timeout value that the Launch Manager will configure on the external watchdog during startup. The external watchdog uses this timeout as the deadline for receiving periodic alive reports from the Launch Manager." - }, - "deactivate_on_shutdown": { - "type": "boolean", - "description": "Specifies whether the Launch Manager disables the external watchdog during shutdown. When set to true, the watchdog is deactivated; when false, it remains active." - }, - "require_magic_close": { - "type": "boolean", - "description": "When true, the Launch Manager will perform a defined shutdown sequence to inform the external watchdog that the shutdown is intentional and to prevent a watchdog-initiated reset." - } - }, - "required": [], - "additionalProperties": false - } - }, - "properties": { - "schema_version": { - "type": "integer", - "description": "Specifies the schema version number that the Launch Manager uses to determine how to parse and validate this configuration file.", - "enum": [ - 1 - ] - }, - "defaults": { - "type": "object", - "description": "Specifies default configuration values that components and Run Targets inherit unless they provide their own overriding values.", - "properties": { - "component_properties": { - "description": "Specifies default component property values applied to all components unless overridden in individual component definitions.", - "$ref": "#/$defs/component_properties" - }, - "deployment_config": { - "description": "Specifies default deployment configuration values applied to all components unless overridden in individual component definitions.", - "$ref": "#/$defs/deployment_config" - }, - "run_target": { - "description": "Specifies default Run Target configuration values applied to all Run Targets unless overridden in individual Run Target definitions.", - "$ref": "#/$defs/run_target" - }, - "alive_supervision": { - "description": "Specifies default alive supervision configuration values that are used unless a global 'alive_supervision' configuration is defined at the root level.", - "$ref": "#/$defs/alive_supervision" - }, - "watchdog": { - "description": "Specifies default watchdog configuration values applied to all watchdogs unless overridden in individual watchdog definitions.", - "$ref": "#/$defs/watchdog" - } - }, - "required": [], - "additionalProperties": false - }, - "components": { - "type": "object", - "description": "Defines software components managed by the Launch Manager, where each property name is a unique component identifier and its value contains the component's configuration.", - "patternProperties": { - "^[a-zA-Z0-9_-]+$": { - "type": "object", - "description": "Specifies an individual component's configuration properties and deployment settings.", - "properties": { - "description": { - "type": "string", - "description": "A human-readable description of the component's purpose." - }, - "component_properties": { - "description": "Specifies component properties for this component; any properties not specified here are inherited from 'defaults.component_properties'.", - "$ref": "#/$defs/component_properties" - }, - "deployment_config": { - "description": "Specifies deployment configuration for this component; any properties not specified here are inherited from 'defaults.deployment_config'.", - "$ref": "#/$defs/deployment_config" - } - }, - "required": [], - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "run_targets": { - "type": "object", - "description": "Defines Run Targets representing different operational modes of the system, where each property name is a unique Run Target identifier and its value contains the Run Target's configuration.", - "patternProperties": { - "^[a-zA-Z0-9_-]+$": { - "$ref": "#/$defs/run_target" - } - }, - "required": [], - "additionalProperties": false - }, - "initial_run_target": { - "type": "string", - "description": "Specifies the initial Run Target name that the Launch Manager activates during startup sequence. This name shall match a Run Target defined in 'run_targets'." - }, - "fallback_run_target": { - "type": "object", - "description": "Defines the fallback Run Target configuration that the Launch Manager activates when all recovery attempts have been exhausted. This Run Target does not include a recovery_action property.", - "properties": { - "description": { - "type": "string", - "description": "A human-readable description of the fallback Run Target." - }, - "depends_on": { - "type": "array", - "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", - "items": { - "type": "string", - "description": "Name of a component or Run Target that this Run Target depends on." - } - }, - "transition_timeout": { - "type": "number", - "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", - "exclusiveMinimum": 0 - } - }, - "required": [], - "additionalProperties": false - }, - "alive_supervision": { - "description": "Defines alive supervision configuration parameters used to monitor component health. This configuration overrides 'defaults.alive_supervision' if specified.", - "$ref": "#/$defs/alive_supervision" - }, - "watchdogs": { - "type": "object", - "description": "Defines external watchdog devices used by the Launch Manager, where each property name is a unique watchdog identifier and its value contains the watchdog's configuration.", - "patternProperties": { - "^[a-zA-Z0-9_-]+$": { - "$ref": "#/$defs/watchdog" - } - }, - "required": [], - "additionalProperties": false - } - }, - "description": "Specifies the structure and valid values for the Launch Manager configuration file, which defines managed components, run targets, and recovery behaviors.", - "required": [ - "schema_version" - ], - "additionalProperties": false -} -""") - score_defaults = json.loads(""" { "deployment_config": { @@ -798,7 +296,7 @@ def get_all_refProcessGroupStates(run_targets): ] hm_config["hmLocalSupervision"].append(local_supervision) - with open(f"{output_dir}/{component_name}.json", "w") as process_file: + with open(f"{output_dir}/hmproc_{component_name}.json", "w") as process_file: process_config = {} process_config["versionMajor"] = HM_SCHEMA_VERSION_MAJOR process_config["versionMinor"] = HM_SCHEMA_VERSION_MINOR @@ -1108,11 +606,17 @@ def get_terminating_behavior(component_config): with open(f"{output_dir}/lm_demo.json", "w") as lm_file: json.dump(lm_config, lm_file, indent=4) +def check_validation_dependency(): + try: + import jsonschema + except ImportError: + print("jsonschema library is not installed. Please install it with \"pip install jsonschema\" to enable schema validation.") + return False + return True def schema_validation(json_input, schema): - from jsonschema import validate, ValidationError - try: + from jsonschema import validate, ValidationError validate(json_input, schema) print("Schema Validation successful") return True @@ -1133,16 +637,30 @@ def main(): action="store_true", help="Only validate the provided configuration file against the schema and exit.", ) + parser.add_argument("--schema", help="Path to the JSON schema file for validation") args = parser.parse_args() input_config = load_json_file(args.filename) - - validation_successful = schema_validation(input_config, json_schema) - if not validation_successful: - exit(1) - - if args.validate: - exit(0) + + if args.schema: + # User asked for validation explicitly, but the dependency is not installed, we should exit with an error + if args.validate and not check_validation_dependency(): + exit(1) + + # User asked not explicitly for validation, but the dependency is not installed, we should print a warning and continue without validation + if not check_validation_dependency(): + print("lifecycle_config.py:jsonschema library is not installed. Please install it with \"pip install jsonschema\" to enable schema validation.") + print("Schema validation will be skipped.") + else: + json_schema = load_json_file(args.schema) + validation_successful = schema_validation(input_config, json_schema) + if not validation_successful: + exit(1) + + if args.validate: + exit(0) + else: + print("No schema provided, skipping validation. Provide the path to the json schema with \"--schema \" to enable validation.") preprocessed_config = preprocess_defaults(score_defaults, input_config) gen_health_monitor_config(args.output_dir, preprocessed_config) diff --git a/src/launch_manager_daemon/BUILD b/src/launch_manager_daemon/BUILD index 26da0e66..b6b2e6f4 100644 --- a/src/launch_manager_daemon/BUILD +++ b/src/launch_manager_daemon/BUILD @@ -16,13 +16,13 @@ package(default_visibility = ["//tests:__subpackages__"]) filegroup( name = "lm_flatcfg_fbs", - srcs = ["config/lm_flatcfg.fbs"], + srcs = ["//src/launch_manager_daemon/config:lm_flatcfg.fbs"], visibility = ["//visibility:public"], ) cc_library( name = "config", - hdrs = ["config/lm_flatcfg_generated.h"], + hdrs = ["//src/launch_manager_daemon/config:lm_flatcfg_generated.h"], includes = ["config"], visibility = ["//src:__pkg__"], ) diff --git a/src/launch_manager_daemon/config/BUILD b/src/launch_manager_daemon/config/BUILD new file mode 100644 index 00000000..4be2d1aa --- /dev/null +++ b/src/launch_manager_daemon/config/BUILD @@ -0,0 +1,5 @@ +exports_files([ + "s-core_launch_manager.schema.json", + "lm_flatcfg.fbs", + "lm_flatcfg_generated.h", +]) diff --git a/src/launch_manager_daemon/config/s-core_launch_manager.schema.json b/src/launch_manager_daemon/config/s-core_launch_manager.schema.json new file mode 100644 index 00000000..fdc93b82 --- /dev/null +++ b/src/launch_manager_daemon/config/s-core_launch_manager.schema.json @@ -0,0 +1,498 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "s-core_launch_manager.schema.json", + "title": "Configuration schema for the S-CORE Launch Manager", + "type": "object", + "$defs": { + "component_properties": { + "type": "object", + "description": "Defines a reusable type that captures the essential development-time characteristics of a software component.", + "properties": { + "binary_name": { + "type": "string", + "description": "Specifies the relative path of the executable file inside the directory defined by 'deployment_config.bin_dir'. The final executable path will be resolved as '{bin_dir}/{binary_name}'. Example values include simple filenames (e.g., 'test_app1') or subdirectory paths (e.g., 'bin/test_app1')." + }, + "application_profile": { + "type": "object", + "description": "Specifies the application profile that defines the runtime behavior and capabilities of this component.", + "properties": { + "application_type": { + "type": "string", + "enum": [ + "Native", + "Reporting", + "Reporting_And_Supervised", + "State_Manager" + ], + "description": "Specifies the level of integration between the component and the Launch Manager. 'Native': no integration with Launch Manager. 'Reporting': uses Launch Manager lifecycle APIs. 'Reporting_And_Supervised': uses lifecycle APIs and sends alive notifications. 'State_Manager': uses lifecycle APIs, sends alive notifications, and has permission to change the active Run Target." + }, + "is_self_terminating": { + "type": "boolean", + "description": "Indicates whether component is designed to terminate automatically once its planned tasks are completed (true), or remain running until explicitly requested to terminate by the Launch Manager (false)." + }, + "alive_supervision": { + "type": "object", + "description": "Specifies the configuration parameters used for alive monitoring of the component.", + "properties": { + "reporting_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the duration of the time interval used to verify that the component sends alive notifications, within the expected time frame." + }, + "failed_cycles_tolerance": { + "type": "integer", + "minimum": 0, + "description": "Defines the maximum number of consecutive reporting cycle failures (see 'reporting_cycle'). Once the number of failed cycles goes above maximum number, Launch Manager will trigger configured recovery action." + }, + "min_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the minimum number of checkpoints that must be reported within each configured 'reporting_cycle'." + }, + "max_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of checkpoints that may be reported within each configured 'reporting_cycle'." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "depends_on": { + "type": "array", + "description": "Names of components that this component depends on. Each dependency must be initialized and reach its ready state before this component can start.", + "items": { + "type": "string", + "description": "Specifies the name of a component on which this component depends." + } + }, + "process_arguments": { + "type": "array", + "description": "Ordered list of command-line arguments passed to the component at startup.", + "items": { + "type": "string", + "description": "Single command-line argument token as a UTF-8 string; order is preserved." + } + }, + "ready_condition": { + "type": "object", + "description": "Specifies the set of conditions that mark when the component completes its initializing state and enters the ready state.", + "properties": { + "process_state": { + "type": "string", + "enum": [ + "Running", + "Terminated" + ], + "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "recovery_action": { + "type": "object", + "description": "Defines a reusable type that specifies which recovery actions should be executed when an error or failure occurs.", + "properties": { + "restart": { + "type": "object", + "description": "Specifies a recovery action that restarts the POSIX process associated with this component.", + "properties": { + "number_of_attempts": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of restart attempts before the Launch Manager concludes that recovery cannot succeed." + }, + "delay_before_restart": { + "type": "number", + "minimum": 0, + "description": "Specifies the delay duration that Launch Manager shall wait before initiating a restart attempt." + } + }, + "required": [], + "additionalProperties": false + }, + "switch_run_target": { + "type": "object", + "description": "Specifies a recovery action that switches to a Run Target. This can be a different Run Target or the same one to retry activation of the current Run Target.", + "properties": { + "run_target": { + "type": "string", + "description": "Specifies the name of the Run Target that Launch Manager should switch to." + } + }, + "required": [], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "restart" + ] + }, + { + "required": [ + "switch_run_target" + ] + } + ], + "additionalProperties": false + }, + "deployment_config": { + "type": "object", + "description": "Defines a reusable type that contains the configuration parameters that are specific to a particular deployment environment or system setup.", + "properties": { + "ready_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time allowed for the component to reach its ready state. The timeout is measured from when the component process is created until the ready conditions specified in 'component_properties.ready_condition' are met." + }, + "shutdown_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum allowed time for the component to terminate after it receives a SIGTERM signal from the Launch Manager. The timeout is measured from the moment the Launch Manager sends the SIGTERM signal, until the Operating System notifies the Launch Manager that the child process has terminated." + }, + "environmental_variables": { + "type": "object", + "description": "Defines the set of environment variables that will be passed to the component at startup.", + "additionalProperties": { + "type": "string", + "description": "Specifies the environment variable's value as a string. An empty string is allowed and represents an intentionally empty environment variable." + } + }, + "bin_dir": { + "type": "string", + "description": "Specifies the absolute filesystem path to the directory where component is installed." + }, + "working_dir": { + "type": "string", + "description": "Specifies the directory that will be used as the working directory for the component during execution." + }, + "ready_recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "restart": true + }, + "required": [ + "restart" + ], + "not": { + "required": [ + "switch_run_target" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component fails to reach its ready state within the configured timeout." + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component malfunctions after reaching its ready state." + }, + "sandbox": { + "type": "object", + "description": "Specifies the sandbox configuration parameters that isolate and constrain the component's runtime execution.", + "properties": { + "uid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the POSIX user ID (UID) under which this component executes." + }, + "gid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the primary POSIX group ID (GID) under which this component executes." + }, + "supplementary_group_ids": { + "type": "array", + "description": "Specifies the list of supplementary POSIX group IDs (GIDs) assigned to this component.", + "items": { + "type": "integer", + "minimum": 0, + "description": "Single supplementary POSIX group ID (GID)" + } + }, + "security_policy": { + "type": "string", + "description": "Specifies the security policy or confinement profile name (such as an SELinux or AppArmor profile) assigned to the component." + }, + "scheduling_policy": { + "type": "string", + "description": "Specifies the scheduling policy applied to the component's initial thread. Supported values correspond to OS-defined policies (e.g., FIFO, RR, OTHER).", + "anyOf": [ + { + "enum": [ + "SCHED_FIFO", + "SCHED_RR", + "SCHED_OTHER" + ] + }, + { + "type": "string" + } + ] + }, + "scheduling_priority": { + "type": "integer", + "description": "Specifies the scheduling priority applied to the component's initial thread." + }, + "max_memory_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum amount of memory, in bytes, that the component is permitted to use during runtime." + }, + "max_cpu_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage of total CPU capacity." + } + }, + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_target": { + "type": "object", + "description": "Defines a reusable type that specifies the configuration parameters for a Run Target.", + "properties": { + "description": { + "type": "string", + "description": "User-defined description of the configured Run Target." + }, + "depends_on": { + "type": "array", + "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", + "exclusiveMinimum": 0 + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when a component assigned to this Run Target fails." + } + }, + "required": [ + "recovery_action" + ], + "additionalProperties": false + }, + "alive_supervision": { + "type": "object", + "description": "Defines a reusable type that contains the configuration parameters for alive supervision", + "properties": { + "evaluation_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the length of the time window used to assess incoming alive supervision reports." + } + }, + "required": [], + "additionalProperties": false + }, + "watchdog": { + "type": "object", + "description": "Defines a reusable type that contains the configuration parameters for the external watchdog.", + "properties": { + "device_file_path": { + "type": "string", + "description": "Path to the external watchdog device file (e.g., /dev/watchdog)." + }, + "max_timeout": { + "type": "number", + "minimum": 0, + "description": "Specifies the maximum timeout value that the Launch Manager will configure on the external watchdog during startup. The external watchdog uses this timeout as the deadline for receiving periodic alive reports from the Launch Manager." + }, + "deactivate_on_shutdown": { + "type": "boolean", + "description": "Specifies whether the Launch Manager disables the external watchdog during shutdown. When set to true, the watchdog is deactivated; when false, it remains active." + }, + "require_magic_close": { + "type": "boolean", + "description": "When true, the Launch Manager will perform a defined shutdown sequence to inform the external watchdog that the shutdown is intentional and to prevent a watchdog-initiated reset." + } + }, + "required": [], + "additionalProperties": false + } + }, + "properties": { + "schema_version": { + "type": "integer", + "description": "Specifies the schema version number that the Launch Manager uses to determine how to parse and validate this configuration file.", + "enum": [ + 1 + ] + }, + "defaults": { + "type": "object", + "description": "Specifies default configuration values that components and Run Targets inherit unless they provide their own overriding values.", + "properties": { + "component_properties": { + "description": "Specifies default component property values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Specifies default deployment configuration values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/deployment_config" + }, + "run_target": { + "description": "Specifies default Run Target configuration values applied to all Run Targets unless overridden in individual Run Target definitions.", + "$ref": "#/$defs/run_target" + }, + "alive_supervision": { + "description": "Specifies default alive supervision configuration values that are used unless a global 'alive_supervision' configuration is defined at the root level.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdog": { + "description": "Specifies default watchdog configuration values applied to all watchdogs unless overridden in individual watchdog definitions.", + "$ref": "#/$defs/watchdog" + } + }, + "required": [], + "additionalProperties": false + }, + "components": { + "type": "object", + "description": "Defines software components managed by the Launch Manager, where each property name is a unique component identifier and its value contains the component's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "description": "Specifies an individual component's configuration properties and deployment settings.", + "properties": { + "description": { + "type": "string", + "description": "A human-readable description of the component's purpose." + }, + "component_properties": { + "description": "Specifies component properties for this component; any properties not specified here are inherited from 'defaults.component_properties'.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Specifies deployment configuration for this component; any properties not specified here are inherited from 'defaults.deployment_config'.", + "$ref": "#/$defs/deployment_config" + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_targets": { + "type": "object", + "description": "Defines Run Targets representing different operational modes of the system, where each property name is a unique Run Target identifier and its value contains the Run Target's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "$ref": "#/$defs/run_target" + } + }, + "required": [], + "additionalProperties": false + }, + "initial_run_target": { + "type": "string", + "description": "Specifies the initial Run Target name that the Launch Manager activates during startup sequence. This name shall match a Run Target defined in 'run_targets'." + }, + "fallback_run_target": { + "type": "object", + "description": "Defines the fallback Run Target configuration that the Launch Manager activates when all recovery attempts have been exhausted. This Run Target does not include a recovery_action property.", + "properties": { + "description": { + "type": "string", + "description": "A human-readable description of the fallback Run Target." + }, + "depends_on": { + "type": "array", + "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", + "exclusiveMinimum": 0 + } + }, + "required": [], + "additionalProperties": false + }, + "alive_supervision": { + "description": "Defines alive supervision configuration parameters used to monitor component health. This configuration overrides 'defaults.alive_supervision' if specified.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdogs": { + "type": "object", + "description": "Defines external watchdog devices used by the Launch Manager, where each property name is a unique watchdog identifier and its value contains the watchdog's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "$ref": "#/$defs/watchdog" + } + }, + "required": [], + "additionalProperties": false + } + }, + "description": "Specifies the structure and valid values for the Launch Manager configuration file, which defines managed components, run targets, and recovery behaviors.", + "required": [ + "schema_version" + ], + "additionalProperties": false +} From 1a2379805e555008f24b3a500451a66be0acc286 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 09:56:43 +0100 Subject: [PATCH 31/72] Undo more changes, fix out of range --- scripts/config_mapping/lifecycle_config.py | 24 +++------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 0570c751..3583c4c6 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -707,29 +707,16 @@ def get_all_refProcessGroupStates(run_targets): run_target_components[rt_name] = supervised_deps index = 0 - # Track which process indices belong to each run target - run_target_indices = {rt: [] for rt in run_target_components} for component_name, component_config in config["components"].items(): if is_supervised(component_config["component_properties"]["application_profile"]["application_type"]): - # Find which run target this component belongs to - component_rt = None - for rt_name, comp_list in run_target_components.items(): - if component_name in comp_list: - component_rt = rt_name - break - process = {} process["index"] = index process["shortName"] = component_name process["identifier"] = component_name process["processType"] = get_process_type(component_config["component_properties"]["application_profile"]["application_type"]) - # Each process references only its own run target's process group state - if component_rt: - process["refProcessGroupStates"] = [{"identifier": "MainPG/" + component_rt}] - else: - process["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) - process["processExecutionErrors"] = [{"processExecutionError":1}] + process["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + process["processExecutionErrors"] = [{"processExecutionError":1} for _ in process["refProcessGroupStates"]] hm_config["process"].append(process) hmMonitorIf = {} @@ -757,10 +744,7 @@ def get_all_refProcessGroupStates(run_targets): alive_supervision["isMaxCheckDisabled"] = alive_supervision["maxAliveIndications"] == 0 alive_supervision["failedSupervisionCyclesTolerance"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["failed_cycles_tolerance"] alive_supervision["refProcessIndex"] = index - if component_rt: - alive_supervision["refProcessGroupStates"] = [{"identifier": "MainPG/" + component_rt}] - else: - alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) hm_config["hmAliveSupervision"].append(alive_supervision) local_supervision = {} @@ -778,8 +762,6 @@ def get_all_refProcessGroupStates(run_targets): process_config["hmMonitorInterface"].append(hmMonitorIf) json.dump(process_config, process_file, indent=4) - if component_rt: - run_target_indices[component_rt].append(index) index += 1 indices = [i for i in range(index)] From 39901a134f3be6232bd36d6b8c10191131ab60e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 23 Feb 2026 10:19:21 +0100 Subject: [PATCH 32/72] Config generation working without pip dependency --- scripts/BUILD | 0 scripts/config_mapping/BUILD | 31 +++------- scripts/config_mapping/config.bzl | 95 ++++++------------------------- 3 files changed, 24 insertions(+), 102 deletions(-) create mode 100644 scripts/BUILD diff --git a/scripts/BUILD b/scripts/BUILD new file mode 100644 index 00000000..e69de29b diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD index 565042f2..859206d3 100644 --- a/scripts/config_mapping/BUILD +++ b/scripts/config_mapping/BUILD @@ -1,31 +1,14 @@ -load("//scripts/config_mapping:config.bzl", "lifecycle_config") -load("//scripts/config_mapping:config.bzl", "lifecycle_config_action") - -exports_files(["lm_config.json"]) - -#sh_binary( -# name = "lifecycle_config", -# srcs = ["lifecycle_config_wrapper.sh"], -# data = [ -# "lifecycle_config.py", -# "requirements.txt", -# ], -# visibility = ["//visibility:public"], -#) - +load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") py_binary( name = "lifecycle_config", srcs = ["lifecycle_config.py"], + visibility = ["//visibility:public"], ) -#lifecycle_config(config_path="//scripts/config_mapping:lm_config.json", -# output_files=["hm_demo.json", "lm_demo.json", "hmcore.json", "someip-daemon.json", -# "state_manager.json", "test_app1.json"]) -# - -lifecycle_config_action( - name ="lifecycle_config_action", - config="//scripts/config_mapping:lm_config.json" -) +# Example Usage +exports_files(["lm_config.json"]) +gen_lifecycle_config( + name ="example_config_gen", + config="//scripts/config_mapping:lm_config.json") diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl index 56f91d29..12e5ce9b 100644 --- a/scripts/config_mapping/config.bzl +++ b/scripts/config_mapping/config.bzl @@ -1,83 +1,22 @@ -#load("//scripts/config_mapping/flatbuffers_rules.bzl", "flatbuffer_json_to_bin") - -def lifecycle_config(config_path, output_files=[]): - - native.genrule( - name = "gen_lifecycle_config", - srcs = [config_path, "//src/launch_manager_daemon/config:s-core_launch_manager.schema.json"], - outs = output_files, - cmd = """ - python3 $(location //scripts/config_mapping:lifecycle_config.py) $(location {input_json}) \ - --schema $(location //src/launch_manager_daemon/config:s-core_launch_manager.schema.json) -o $(@D) - """.format(input_json=config_path), - - tools = ["//scripts/config_mapping:lifecycle_config.py"], - visibility = ["//visibility:public"], - ) - -#def _flatbuffer_json_to_bin_impl_tmp(ctx): -# flatc = ctx.executable.flatc -# json = ctx.file.json -# schema = ctx.file.schema -# -# # flatc will name the file the same as the json (can't be changed) -# out_name = json.basename[:-len(".json")] + ".bin" -# out = ctx.actions.declare_file(out_name, sibling = json) -# -# # flatc args --------------------------------- -# flatc_args = [ -# "-b", -# "-o", -# out.dirname, -# ] -# -# for inc in ctx.attr.includes: -# flatc_args.extend(["-I", inc.path]) -# -# if ctx.attr.strict_json: -# flatc_args.append("--strict-json") -# -# flatc_args.extend([schema.path, json.path]) -# # -------------------------------------------- -# -# ctx.actions.run( -# inputs = [json, schema] + list(ctx.files.includes), -# outputs = [out], -# executable = flatc, -# arguments = flatc_args, -# progress_message = "flatc generation {}".format(json.short_path), -# mnemonic = "FlatcGeneration", -# ) -# -# rf = ctx.runfiles( -# files = [out], -# root_symlinks = { -# ("_main/" + ctx.attr.out_dir + "/" + out_name): out, -# }, -# ) - -def _lifecycle_config_impl(ctx): +def _gen_lifecycle_config_impl(ctx): config = ctx.file.config schema = ctx.file.schema script = ctx.executable.script json_out_dir = ctx.attr.json_out_dir - # Get Python runtime - python_runtime = ctx.toolchains["@rules_python//python:toolchain_type"].py3_runtime - python_exe = python_runtime.interpreter - - # First run_shell - creates directory with files inside + # Run the mapping script to generate the json files in the old configuration format + # We need to declare an output directory, because we do not know upfront the name of the generated files nor the number of files. gen_dir_json = ctx.actions.declare_directory(json_out_dir) - ctx.actions.run_shell( + ctx.actions.run( inputs = [config, schema], outputs = [gen_dir_json], - tools = [script, python_exe], - command = """ - export PYTHON3={} - export PATH="$(dirname {}):$PATH" - {} {} --schema {} -o {} - """.format(python_exe.path, python_exe.path, script.path, config.path, schema.path, gen_dir_json.path), - arguments = [] + tools = [script], + executable = script, + arguments = [ + config.path, + "--schema", schema.path, + "-o", gen_dir_json.path + ] ) flatbuffer_out_dir = ctx.attr.flatbuffer_out_dir @@ -86,7 +25,8 @@ def _lifecycle_config_impl(ctx): hm_schema = ctx.file.hm_schema hmcore_schema = ctx.file.hmcore_schema - # Second run_shell - processes the files from the generated directory + # We compile each of them via flatbuffer. + # Based on the name of each generated file, we select the corresponding schema. gen_dir_flatbuffer = ctx.actions.declare_directory(flatbuffer_out_dir) ctx.actions.run_shell( inputs = [gen_dir_json, lm_schema, hm_schema, hmcore_schema], @@ -127,11 +67,11 @@ def _lifecycle_config_impl(ctx): arguments = [] ) - return DefaultInfo(files = depset([gen_dir_json, gen_dir_flatbuffer])) + return DefaultInfo(files = depset([gen_dir_flatbuffer])) -lifecycle_config_action = rule( - implementation = _lifecycle_config_impl, +gen_lifecycle_config = rule( + implementation = _gen_lifecycle_config_impl, attrs = { "config": attr.label( allow_single_file = [".json"], @@ -178,7 +118,6 @@ lifecycle_config_action = rule( default=Label("//src/launch_manager_daemon/health_monitor_lib:hmcore_flatcfg_fbs"), doc = "HealthMonitor core fbs file to use", ) - }, - toolchains = ["@rules_python//python:toolchain_type"], + } ) From 703c05bc5ca2d7df50d137a87118bc0d6350597f Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 10:03:13 +0100 Subject: [PATCH 33/72] More cleanup --- examples/config/gen_lifecycle_config.py | 3 +++ scripts/config_mapping/lifecycle_config.py | 19 ++----------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/examples/config/gen_lifecycle_config.py b/examples/config/gen_lifecycle_config.py index 419858cf..9931823a 100644 --- a/examples/config/gen_lifecycle_config.py +++ b/examples/config/gen_lifecycle_config.py @@ -77,6 +77,9 @@ def gen_lifecycle_config( "binary_name": "control_app/control_daemon", "application_profile": { "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0, + }, }, }, "deployment_config": { diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 3583c4c6..306117a5 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -663,7 +663,7 @@ def get_process_type(application_type): return "REGULAR_PROCESS" def is_supervised(application_type): - return application_type == "Reporting_And_Supervised" + return application_type == "State_Manager" or application_type == "Reporting_And_Supervised" def get_all_process_group_states(run_targets): process_group_states = [] @@ -692,22 +692,7 @@ def get_all_refProcessGroupStates(run_targets): hm_config["hmLocalSupervision"] = [] hm_config["hmGlobalSupervision"] = [] hm_config["hmRecoveryNotification"] = [] - # Build a mapping of run_target -> list of supervised component names - run_target_components = {} - for rt_name, rt_config in config["run_targets"].items(): - if rt_name == "Off": - continue - supervised_deps = [] - for dep_name in rt_config.get("depends_on", []): - if dep_name in config["components"]: - comp = config["components"][dep_name] - if is_supervised(comp["component_properties"]["application_profile"]["application_type"]): - supervised_deps.append(dep_name) - if supervised_deps: - run_target_components[rt_name] = supervised_deps - index = 0 - for component_name, component_config in config["components"].items(): if is_supervised(component_config["component_properties"]["application_profile"]["application_type"]): process = {} @@ -776,7 +761,7 @@ def get_all_refProcessGroupStates(run_targets): hm_config["hmGlobalSupervision"].append(global_supervision) recovery_action = {} - recovery_action["shortName"] = f"RecoveryNotification_{rt_name}" + recovery_action["shortName"] = f"recovery_notification" recovery_action["recoveryNotificationTimeout"] = 5000 recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(global_supervision) From afd2c7d71b5f94d08cd459cd80c65debc8b16a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 23 Feb 2026 10:53:26 +0100 Subject: [PATCH 34/72] Export jsonschema python dependency --- MODULE.bazel | 9 +++++++++ requirements.in | 2 ++ requirements_lock.txt | 6 ++++++ scripts/config_mapping/BUILD | 2 ++ scripts/config_mapping/Readme.md | 2 +- scripts/config_mapping/requirements_external.txt | 1 + .../{requirements.txt => requirements_internal.txt} | 0 7 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 requirements.in create mode 100644 requirements_lock.txt create mode 100644 scripts/config_mapping/requirements_external.txt rename scripts/config_mapping/{requirements.txt => requirements_internal.txt} (100%) diff --git a/MODULE.bazel b/MODULE.bazel index 6c09448e..29cbfb6e 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -113,6 +113,15 @@ python.toolchain( ) use_repo(python) +# Python pip dependencies +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.parse( + hub_name = "score_lifecycle_pip", + python_version = PYTHON_VERSION, + requirements_lock = "//:requirements_lock.txt", +) +use_repo(pip, "score_lifecycle_pip") + bazel_dep(name = "score_baselibs_rust", version = "0.1.0") bazel_dep(name = "score_baselibs", version = "0.2.4") diff --git a/requirements.in b/requirements.in new file mode 100644 index 00000000..45314edc --- /dev/null +++ b/requirements.in @@ -0,0 +1,2 @@ +# Python dependencies for generating lifecycle configuration files +-r ./scripts/config_mapping/requirements_external.txt diff --git a/requirements_lock.txt b/requirements_lock.txt new file mode 100644 index 00000000..7d799e4a --- /dev/null +++ b/requirements_lock.txt @@ -0,0 +1,6 @@ +attrs==25.4.0 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +referencing==0.37.0 +rpds-py==0.30.0 +typing_extensions==4.15.0 \ No newline at end of file diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD index 859206d3..ff35ec38 100644 --- a/scripts/config_mapping/BUILD +++ b/scripts/config_mapping/BUILD @@ -1,9 +1,11 @@ load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") +load("@score_lifecycle_pip//:requirements.bzl", "requirement") py_binary( name = "lifecycle_config", srcs = ["lifecycle_config.py"], + deps = [requirement("jsonschema")], visibility = ["//visibility:public"], ) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 05859989..77a330a4 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -25,7 +25,7 @@ You may want to use the virtual environment: ```bash python3 -m venv myvenv . myvenv/bin/activate -pip3 install -r requirements.txt +pip3 install -r requirements_internal.txt ``` Execute tests: diff --git a/scripts/config_mapping/requirements_external.txt b/scripts/config_mapping/requirements_external.txt new file mode 100644 index 00000000..7b8f0158 --- /dev/null +++ b/scripts/config_mapping/requirements_external.txt @@ -0,0 +1 @@ +jsonschema \ No newline at end of file diff --git a/scripts/config_mapping/requirements.txt b/scripts/config_mapping/requirements_internal.txt similarity index 100% rename from scripts/config_mapping/requirements.txt rename to scripts/config_mapping/requirements_internal.txt From 5bb5fba80ec0ff23ec967b41b3c302e6c9a21d03 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 11:13:30 +0100 Subject: [PATCH 35/72] Fix python formatting --- examples/config/gen_lifecycle_config.py | 1 + scripts/config_mapping/integration_tests.py | 51 ++-- scripts/config_mapping/lifecycle_config.py | 319 ++++++++++++++------ scripts/config_mapping/unit_tests.py | 17 +- 4 files changed, 266 insertions(+), 122 deletions(-) diff --git a/examples/config/gen_lifecycle_config.py b/examples/config/gen_lifecycle_config.py index 9931823a..97e34c32 100644 --- a/examples/config/gen_lifecycle_config.py +++ b/examples/config/gen_lifecycle_config.py @@ -19,6 +19,7 @@ python3 scripts/config_mapping/lifecycle_config.py -o """ + import argparse import json import os diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 7db174fd..1cd8f1c6 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -9,7 +9,8 @@ tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" -def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files=[]): + +def run(input_file: Path, test_name: str, compare_files_only=[], exclude_files=[]): """ Execute the mapping script with the given input file and compare the generated output with the expected output. Input: @@ -20,7 +21,9 @@ def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files expected_output_dir = tests_dir / test_name / "expected_output" if compare_files_only and exclude_files: - raise AssertionError("You may only make use of either parameters: compare_files_only or exclude_files, but not both.") + raise AssertionError( + "You may only make use of either parameters: compare_files_only or exclude_files, but not both." + ) # Clean and create actual output directory if actual_output_dir.exists(): @@ -29,10 +32,11 @@ def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files # Execute lifecycle_config.py cmd = [ - "python3", + "python3", str(lifecycle_script), str(input_file), - "-o", str(actual_output_dir) + "-o", + str(actual_output_dir), ] try: @@ -46,33 +50,41 @@ def run(input_file : Path, test_name : str, compare_files_only=[], exclude_files if compare_files_only: # Compare only specific files - if not compare_files(actual_output_dir, expected_output_dir, compare_files_only): - raise AssertionError("Actual output files do not match expected output files.") + if not compare_files( + actual_output_dir, expected_output_dir, compare_files_only + ): + raise AssertionError( + "Actual output files do not match expected output files." + ) else: # Compare the complete directory content - if not compare_directories(actual_output_dir, expected_output_dir, exclude_files): + if not compare_directories( + actual_output_dir, expected_output_dir, exclude_files + ): raise AssertionError("Actual output does not match expected output.") -def compare_directories(dir1: Path, dir2: Path, exclude_files : list) -> bool: + +def compare_directories(dir1: Path, dir2: Path, exclude_files: list) -> bool: """ Compare two directories recursively. Return True if they are the same, False otherwise. """ dcmp = filecmp.dircmp(dir1, dir2, ignore=exclude_files) - + if dcmp.left_only or dcmp.right_only or dcmp.diff_files: print(f"Directories differ: {dir1} vs {dir2}") print(f"Only in {dir1}: {dcmp.left_only}") print(f"Only in {dir2}: {dcmp.right_only}") print(f"Different files: {dcmp.diff_files}") return False - + for common_dir in dcmp.common_dirs: if not compare_directories(dir1 / common_dir, dir2 / common_dir): return False - + return True -def compare_files(dir1 : Path, dir2 : Path, files : list) -> bool: + +def compare_files(dir1: Path, dir2: Path, files: list) -> bool: """ Compare specific files in two directories. Return True if they are the same, False otherwise. """ @@ -84,6 +96,7 @@ def compare_files(dir1 : Path, dir2 : Path, files : list) -> bool: return False return True + def test_basic(): """ Basic Smoketest for generating both launch manager and health monitoring configuration @@ -93,6 +106,7 @@ def test_basic(): run(input_file, test_name) + def test_health_config_mapping(): """ Test generation of the health monitoring configuration with @@ -102,18 +116,20 @@ def test_health_config_mapping(): """ test_name = "health_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - + run(input_file, test_name, exclude_files=["lm_demo.json"]) + def test_empty_health_config_mapping(): """ Test generation of the health monitoring configuration with no supervised processes """ test_name = "empty_health_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - + run(input_file, test_name, exclude_files=["lm_demo.json"]) + def test_launch_config_mapping(): """ Test generation of the launch manager configuration with @@ -123,14 +139,15 @@ def test_launch_config_mapping(): """ test_name = "lm_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - + run(input_file, test_name, compare_files_only=["lm_demo.json"]) + def test_empty_launch_config_mapping(): """ Test generation of the launch manager configuration with no processes defined """ test_name = "empty_lm_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - - run(input_file, test_name, compare_files_only=["lm_demo.json"]) \ No newline at end of file + + run(input_file, test_name, compare_files_only=["lm_demo.json"]) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 306117a5..0d30997e 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -7,7 +7,7 @@ from typing import Dict, Any # TODO -json_schema = json.loads(''' +json_schema = json.loads(""" { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "s-core_launch_manager.schema.json", @@ -506,9 +506,9 @@ ], "additionalProperties": false } -''') +""") -score_defaults = json.loads(''' +score_defaults = json.loads(""" { "deployment_config": { "ready_timeout": 0.5, @@ -558,25 +558,28 @@ }, "watchdogs": {} } -''') +""") # There are various dictionaries in the config where only a single entry is allowed. # We do not want to merge the defaults with the user specified values for these dictionaries. not_merging_dicts = ["ready_recovery_action", "recovery_action"] + def load_json_file(file_path: str) -> Dict[str, Any]: """Load and parse a JSON file.""" - with open(file_path, 'r') as file: + with open(file_path, "r") as file: return json.load(file) + def get_recovery_process_group_state(config): fallback = config.get("fallback_run_target", None) if fallback: - return "MainPG/fallback_run_target" + return "MainPG/fallback_run_target" else: return "MainPG/Off" -def sec_to_ms(sec : float) -> int: + +def sec_to_ms(sec: float) -> int: return int(sec * 1000) @@ -585,10 +588,15 @@ def preprocess_defaults(global_defaults, config): This function takes the input configuration and fills in any missing fields with default values. The resulting file with have no "defaults" entry anymore, but looks like if the user had specified all the fields explicitly. """ + def dict_merge(dict_a, dict_b): def dict_merge_recursive(dict_a, dict_b): for key, value in dict_b.items(): - if key in dict_a and isinstance(dict_a[key], dict) and isinstance(value, dict): + if ( + key in dict_a + and isinstance(dict_a[key], dict) + and isinstance(value, dict) + ): # For certain dictionaries, we do not want to merge the defaults with the user specified values if key in not_merging_dicts: dict_a[key] = value @@ -600,11 +608,12 @@ def dict_merge_recursive(dict_a, dict_b): else: # For lists, we want to overwrite the content if isinstance(value, list): - dict_a[key] = (value) + dict_a[key] = value # For primitive types, we want to take the one from dict_b else: dict_a[key] = value return dict_a + # We are changing the content of dict_a, so we need a deep copy return dict_merge_recursive(deepcopy(dict_a), dict_b) @@ -617,12 +626,20 @@ def dict_merge_recursive(dict_a, dict_b): new_config["components"] = {} components = config.get("components", {}) for component_name, component_config in components.items(): - #print("Processing component:", component_name) + # print("Processing component:", component_name) new_config["components"][component_name] = {} - new_config["components"][component_name]["description"] = component_config.get("description", "") + new_config["components"][component_name]["description"] = component_config.get( + "description", "" + ) # Here we start with the merged defaults, then apply the component config on top, so that any fields specified in the component config will override the defaults. - new_config["components"][component_name]["component_properties"] = dict_merge(merged_defaults["component_properties"], component_config.get("component_properties")) - new_config["components"][component_name]["deployment_config"] = dict_merge(merged_defaults["deployment_config"], component_config.get("deployment_config", {})) + new_config["components"][component_name]["component_properties"] = dict_merge( + merged_defaults["component_properties"], + component_config.get("component_properties"), + ) + new_config["components"][component_name]["deployment_config"] = dict_merge( + merged_defaults["deployment_config"], + component_config.get("deployment_config", {}), + ) # Special case: # If the defaults specify alive_supervision for component, but the component config sets the type to anything other than "SUPERVISED", then we should not apply the @@ -631,31 +648,39 @@ def dict_merge_recursive(dict_a, dict_b): new_config["run_targets"] = {} for run_target, run_target_config in config.get("run_targets", {}).items(): - new_config["run_targets"][run_target] = dict_merge(merged_defaults["run_target"], run_target_config) + new_config["run_targets"][run_target] = dict_merge( + merged_defaults["run_target"], run_target_config + ) - new_config["alive_supervision"] = dict_merge(merged_defaults["alive_supervision"], config.get("alive_supervision", {})) - new_config["watchdogs"] = dict_merge(merged_defaults["watchdogs"], config.get("watchdogs", {})) + new_config["alive_supervision"] = dict_merge( + merged_defaults["alive_supervision"], config.get("alive_supervision", {}) + ) + new_config["watchdogs"] = dict_merge( + merged_defaults["watchdogs"], config.get("watchdogs", {}) + ) for key in ("initial_run_target", "fallback_run_target"): if key in config: new_config[key] = config[key] - #print(json.dumps(new_config, indent=4)) + # print(json.dumps(new_config, indent=4)) return new_config + def gen_health_monitor_config(output_dir, config): """ This function generates the health monitor configuration file based on the input configuration. Input: output_dir: The directory where the generated files should be saved - config: The preprocessed configuration in the new format, with all defaults applied + config: The preprocessed configuration in the new format, with all defaults applied Output: - A file named "hm_demo.json" containing the health monitor daemon configuration - A optional file named "hmcore.json" containing the watchdog configuration - For each supervised process, a file named "_.json" """ + def get_process_type(application_type): if application_type == "State_Manager": return "STM_PROCESS" @@ -663,12 +688,15 @@ def get_process_type(application_type): return "REGULAR_PROCESS" def is_supervised(application_type): - return application_type == "State_Manager" or application_type == "Reporting_And_Supervised" + return ( + application_type == "State_Manager" + or application_type == "Reporting_And_Supervised" + ) def get_all_process_group_states(run_targets): process_group_states = [] for run_target, _ in run_targets.items(): - process_group_states.append("MainPG/"+run_target) + process_group_states.append("MainPG/" + run_target) return process_group_states def get_all_refProcessGroupStates(run_targets): @@ -677,13 +705,13 @@ def get_all_refProcessGroupStates(run_targets): for state in states: refProcessGroupStates.append({"identifier": state}) return refProcessGroupStates - + HM_SCHEMA_VERSION_MAJOR = 8 HM_SCHEMA_VERSION_MINOR = 0 hm_config = {} hm_config["versionMajor"] = HM_SCHEMA_VERSION_MAJOR hm_config["versionMinor"] = HM_SCHEMA_VERSION_MINOR - hm_config["process"]= [] + hm_config["process"] = [] hm_config["hmMonitorInterface"] = [] hm_config["hmSupervisionCheckpoint"] = [] hm_config["hmAliveSupervision"] = [] @@ -694,14 +722,26 @@ def get_all_refProcessGroupStates(run_targets): hm_config["hmRecoveryNotification"] = [] index = 0 for component_name, component_config in config["components"].items(): - if is_supervised(component_config["component_properties"]["application_profile"]["application_type"]): + if is_supervised( + component_config["component_properties"]["application_profile"][ + "application_type" + ] + ): process = {} process["index"] = index process["shortName"] = component_name process["identifier"] = component_name - process["processType"] = get_process_type(component_config["component_properties"]["application_profile"]["application_type"]) - process["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) - process["processExecutionErrors"] = [{"processExecutionError":1} for _ in process["refProcessGroupStates"]] + process["processType"] = get_process_type( + component_config["component_properties"]["application_profile"][ + "application_type" + ] + ) + process["refProcessGroupStates"] = get_all_refProcessGroupStates( + config["run_targets"] + ) + process["processExecutionErrors"] = [ + {"processExecutionError": 1} for _ in process["refProcessGroupStates"] + ] hm_config["process"].append(process) hmMonitorIf = {} @@ -710,7 +750,9 @@ def get_all_refProcessGroupStates(run_targets): hmMonitorIf["portPrototype"] = "DefaultPort" hmMonitorIf["interfacePath"] = "lifecycle_health_" + component_name hmMonitorIf["refProcessIndex"] = index - hmMonitorIf["permittedUid"] = component_config["deployment_config"]["sandbox"]["uid"] + hmMonitorIf["permittedUid"] = component_config["deployment_config"][ + "sandbox" + ]["uid"] hm_config["hmMonitorInterface"].append(hmMonitorIf) checkpoint = {} @@ -722,20 +764,38 @@ def get_all_refProcessGroupStates(run_targets): alive_supervision = {} alive_supervision["ruleContextKey"] = component_name + "_alive_supervision" alive_supervision["refCheckPointIndex"] = index - alive_supervision["aliveReferenceCycle"] = sec_to_ms(component_config["component_properties"]["application_profile"]["alive_supervision"]["reporting_cycle"]) - alive_supervision["minAliveIndications"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["min_indications"] - alive_supervision["maxAliveIndications"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["max_indications"] - alive_supervision["isMinCheckDisabled"] = alive_supervision["minAliveIndications"] == 0 - alive_supervision["isMaxCheckDisabled"] = alive_supervision["maxAliveIndications"] == 0 - alive_supervision["failedSupervisionCyclesTolerance"] = component_config["component_properties"]["application_profile"]["alive_supervision"]["failed_cycles_tolerance"] + alive_supervision["aliveReferenceCycle"] = sec_to_ms( + component_config["component_properties"]["application_profile"][ + "alive_supervision" + ]["reporting_cycle"] + ) + alive_supervision["minAliveIndications"] = component_config[ + "component_properties" + ]["application_profile"]["alive_supervision"]["min_indications"] + alive_supervision["maxAliveIndications"] = component_config[ + "component_properties" + ]["application_profile"]["alive_supervision"]["max_indications"] + alive_supervision["isMinCheckDisabled"] = ( + alive_supervision["minAliveIndications"] == 0 + ) + alive_supervision["isMaxCheckDisabled"] = ( + alive_supervision["maxAliveIndications"] == 0 + ) + alive_supervision["failedSupervisionCyclesTolerance"] = component_config[ + "component_properties" + ]["application_profile"]["alive_supervision"]["failed_cycles_tolerance"] alive_supervision["refProcessIndex"] = index - alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates( + config["run_targets"] + ) hm_config["hmAliveSupervision"].append(alive_supervision) local_supervision = {} local_supervision["ruleContextKey"] = component_name + "_local_supervision" local_supervision["infoRefInterfacePath"] = "" - local_supervision["hmRefAliveSupervision"] = [{"refAliveSupervisionIdx": index}] + local_supervision["hmRefAliveSupervision"] = [ + {"refAliveSupervisionIdx": index} + ] hm_config["hmLocalSupervision"].append(local_supervision) with open(f"{output_dir}/{component_name}.json", "w") as process_file: @@ -755,16 +815,24 @@ def get_all_refProcessGroupStates(run_targets): global_supervision = {} global_supervision["ruleContextKey"] = "global_supervision" global_supervision["isSeverityCritical"] = False - global_supervision["localSupervision"] = [{"refLocalSupervisionIndex": idx} for idx in indices] + global_supervision["localSupervision"] = [ + {"refLocalSupervisionIndex": idx} for idx in indices + ] global_supervision["refProcesses"] = [{"index": idx} for idx in indices] - global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates(config["run_targets"]) + global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates( + config["run_targets"] + ) hm_config["hmGlobalSupervision"].append(global_supervision) recovery_action = {} recovery_action["shortName"] = f"recovery_notification" recovery_action["recoveryNotificationTimeout"] = 5000 - recovery_action["processGroupMetaModelIdentifier"] = get_recovery_process_group_state(config) - recovery_action["refGlobalSupervisionIndex"] = hm_config["hmGlobalSupervision"].index(global_supervision) + recovery_action["processGroupMetaModelIdentifier"] = ( + get_recovery_process_group_state(config) + ) + recovery_action["refGlobalSupervisionIndex"] = hm_config[ + "hmGlobalSupervision" + ].index(global_supervision) recovery_action["instanceSpecifier"] = "" recovery_action["shouldFireWatchdog"] = False hm_config["hmRecoveryNotification"].append(recovery_action) @@ -778,9 +846,13 @@ def get_all_refProcessGroupStates(run_targets): hmcore_config["versionMajor"] = HM_CORE_SCHEMA_VERSION_MAJOR hmcore_config["versionMinor"] = HM_CORE_SCHEMA_VERSION_MINOR hmcore_config["watchdogs"] = [] - hmcore_config["config"] = [{ - "periodicity": sec_to_ms(config.get("alive_supervision", {}).get("evaluation_cycle", 0.01)) - }] + hmcore_config["config"] = [ + { + "periodicity": sec_to_ms( + config.get("alive_supervision", {}).get("evaluation_cycle", 0.01) + ) + } + ] for watchdog_name, watchdog_config in config.get("watchdogs", {}).items(): watchdog = {} watchdog["shortName"] = watchdog_name @@ -801,20 +873,23 @@ def gen_launch_manager_config(output_dir, config): This function generates the launch manager configuration file based on the input configuration. Input: output_dir: The directory where the generated files should be saved - config: The preprocessed configuration in the new format, with all defaults applied + config: The preprocessed configuration in the new format, with all defaults applied Output: - A file named "lm_demo.json" containing the launch manager configuration """ - + """ Recursively get all components on which the run target depends """ + def format_dependency_path(path, cycle_target): """Format a dependency resolution path for display, highlighting the cycle.""" return " -> ".join(path + [cycle_target]) - def get_process_dependencies(run_target, ancestors_run_targets=None, ancestors_components=None): + def get_process_dependencies( + run_target, ancestors_run_targets=None, ancestors_components=None + ): """ Resolve all component dependencies for the given run target. @@ -842,7 +917,9 @@ def get_process_dependencies(run_target, ancestors_run_targets=None, ancestors_c ancestors_components.append(dependency_name) out.append(dependency_name) - component_props = config["components"][dependency_name]["component_properties"] + component_props = config["components"][dependency_name][ + "component_properties" + ] if "depends_on" in component_props: # All dependencies must be components, since components can't depend on run targets for dep in component_props["depends_on"]: @@ -873,7 +950,9 @@ def get_process_dependencies(run_target, ancestors_run_targets=None, ancestors_c f"Run target depends on unknown run target '{dependency_name}'." ) if dependency_name in ancestors_run_targets: - path = format_dependency_path(ancestors_run_targets, dependency_name) + path = format_dependency_path( + ancestors_run_targets, dependency_name + ) raise ValueError( f"Cyclic dependency detected: run target '{dependency_name}' " f"has already been visited.\n Path: {path}" @@ -888,44 +967,49 @@ def get_process_dependencies(run_target, ancestors_run_targets=None, ancestors_c return list(set(out)) # Remove duplicates def get_terminating_behavior(component_config): - if component_config["component_properties"]["application_profile"]["is_self_terminating"]: + if component_config["component_properties"]["application_profile"][ + "is_self_terminating" + ]: return "ProcessIsSelfTerminating" else: return "ProcessIsNotSelfTerminating" - if 'fallback_run_target' in config['run_targets']: - print('Run target name fallback_run_target is reserved at the moment', file=sys.stderr) + if "fallback_run_target" in config["run_targets"]: + print( + "Run target name fallback_run_target is reserved at the moment", + file=sys.stderr, + ) exit(1) lm_config = {} lm_config["versionMajor"] = 7 lm_config["versionMinor"] = 0 lm_config["Process"] = [] - lm_config["ModeGroup"] = [{ - "identifier": "MainPG", - "initialMode_name": config.get("initial_run_target", "Off"), - "recoveryMode_name": get_recovery_process_group_state(config), - "modeDeclaration": [] - }] + lm_config["ModeGroup"] = [ + { + "identifier": "MainPG", + "initialMode_name": config.get("initial_run_target", "Off"), + "recoveryMode_name": get_recovery_process_group_state(config), + "modeDeclaration": [], + } + ] process_group_states = {} # For each component, store which run targets depends on it for pgstate, values in config["run_targets"].items(): state_name = "MainPG/" + pgstate - lm_config["ModeGroup"][0]["modeDeclaration"].append({ - "identifier": state_name - }) + lm_config["ModeGroup"][0]["modeDeclaration"].append({"identifier": state_name}) components = get_process_dependencies(values) for component in components: if component not in process_group_states: process_group_states[component] = [] process_group_states[component].append(state_name) - if (fallback := config.get("fallback_run_target", {})): - lm_config["ModeGroup"][0]["modeDeclaration"].append({ - "identifier": "MainPG/fallback_run_target" - }) + if fallback := config.get("fallback_run_target", {}): + lm_config["ModeGroup"][0]["modeDeclaration"].append( + {"identifier": "MainPG/fallback_run_target"} + ) fallback_components = get_process_dependencies(fallback) for component in fallback_components: if component not in process_group_states: @@ -935,69 +1019,99 @@ def get_terminating_behavior(component_config): for component_name, component_config in config["components"].items(): process = {} process["identifier"] = component_name - process["path"] = f'{component_config["deployment_config"]["bin_dir"]}/{component_config["component_properties"]["binary_name"]}' + process["path"] = ( + f"{component_config['deployment_config']['bin_dir']}/{component_config['component_properties']['binary_name']}" + ) process["uid"] = component_config["deployment_config"]["sandbox"]["uid"] process["gid"] = component_config["deployment_config"]["sandbox"]["gid"] - process["sgids"] = [{"sgid": sgid} for sgid in component_config["deployment_config"]["sandbox"]["supplementary_group_ids"]] - process["securityPolicyDetails"] = component_config["deployment_config"]["sandbox"]["security_policy"] - process["numberOfRestartAttempts"] = component_config["deployment_config"]["ready_recovery_action"]["restart"]["number_of_attempts"] + process["sgids"] = [ + {"sgid": sgid} + for sgid in component_config["deployment_config"]["sandbox"][ + "supplementary_group_ids" + ] + ] + process["securityPolicyDetails"] = component_config["deployment_config"][ + "sandbox" + ]["security_policy"] + process["numberOfRestartAttempts"] = component_config["deployment_config"][ + "ready_recovery_action" + ]["restart"]["number_of_attempts"] - match component_config["component_properties"]["application_profile"]["application_type"]: + match component_config["component_properties"]["application_profile"][ + "application_type" + ]: case "Native": - process["executable_reportingBehavior"] = "DoesNotReportExecutionState" + process["executable_reportingBehavior"] = "DoesNotReportExecutionState" case "State_Manager": - process["executable_reportingBehavior"] = "ReportsExecutionState" + process["executable_reportingBehavior"] = "ReportsExecutionState" process["functionClusterAffiliation"] = "STATE_MANAGEMENT" case "Reporting" | "Reporting_And_Supervised": - process["executable_reportingBehavior"] = "ReportsExecutionState" + process["executable_reportingBehavior"] = "ReportsExecutionState" process["startupConfig"] = [{}] process["startupConfig"][0]["executionError"] = "1" process["startupConfig"][0]["identifier"] = f"{component_name}_startup_config" - process["startupConfig"][0]["enterTimeoutValue"] = sec_to_ms(component_config["deployment_config"]["ready_timeout"]) - process["startupConfig"][0]["exitTimeoutValue"] = sec_to_ms(component_config["deployment_config"]["shutdown_timeout"]) - process["startupConfig"][0]["schedulingPolicy"] = component_config["deployment_config"]["sandbox"]["scheduling_policy"] - process["startupConfig"][0]["schedulingPriority"] = str(component_config["deployment_config"]["sandbox"]["scheduling_priority"]) - process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior(component_config) + process["startupConfig"][0]["enterTimeoutValue"] = sec_to_ms( + component_config["deployment_config"]["ready_timeout"] + ) + process["startupConfig"][0]["exitTimeoutValue"] = sec_to_ms( + component_config["deployment_config"]["shutdown_timeout"] + ) + process["startupConfig"][0]["schedulingPolicy"] = component_config[ + "deployment_config" + ]["sandbox"]["scheduling_policy"] + process["startupConfig"][0]["schedulingPriority"] = str( + component_config["deployment_config"]["sandbox"]["scheduling_priority"] + ) + process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior( + component_config + ) process["startupConfig"][0]["processGroupStateDependency"] = [] process["startupConfig"][0]["environmentVariable"] = [] - for env_var, value in component_config["deployment_config"].get("environmental_variables",{}).items(): - process["startupConfig"][0]["environmentVariable"].append({ - "key": env_var, - "value": value - }) + for env_var, value in ( + component_config["deployment_config"] + .get("environmental_variables", {}) + .items() + ): + process["startupConfig"][0]["environmentVariable"].append( + {"key": env_var, "value": value} + ) - if (arguments := component_config["component_properties"].get("process_arguments", [])): + if arguments := component_config["component_properties"].get( + "process_arguments", [] + ): arguments = [{"argument": arg} for arg in arguments] process["startupConfig"][0]["processArgument"] = arguments if component_name in process_group_states: for pgstate in process_group_states[component_name]: - process["startupConfig"][0]["processGroupStateDependency"].append({ - "stateMachine_name": "MainPG", - "stateName": pgstate - }) + process["startupConfig"][0]["processGroupStateDependency"].append( + {"stateMachine_name": "MainPG", "stateName": pgstate} + ) lm_config["Process"].append(process) - + # Execution dependencies. Assumption: Components can never depend on run targets for process in lm_config["Process"]: process["startupConfig"][0]["executionDependency"] = [] - for dependency in config["components"][process["identifier"]]["component_properties"].get("depends_on", []): + for dependency in config["components"][process["identifier"]][ + "component_properties" + ].get("depends_on", []): dep_entry = config["components"][dependency] - ready_condition = dep_entry["component_properties"]["ready_condition"]["process_state"] - process["startupConfig"][0]["executionDependency"].append({ - "stateName": ready_condition, - "targetProcess_identifier": dependency - }) - - + ready_condition = dep_entry["component_properties"]["ready_condition"][ + "process_state" + ] + process["startupConfig"][0]["executionDependency"].append( + {"stateName": ready_condition, "targetProcess_identifier": dependency} + ) + with open(f"{output_dir}/lm_demo.json", "w") as lm_file: json.dump(lm_config, lm_file, indent=4) def schema_validation(json_input, schema): from jsonschema import validate, ValidationError + try: validate(json_input, schema) print("Schema Validation successful") @@ -1006,11 +1120,19 @@ def schema_validation(json_input, schema): print(err) return False + def main(): parser = argparse.ArgumentParser() parser.add_argument("filename", help="The launch_manager configuration file") - parser.add_argument("--output-dir", "-o", default=".", help="Output directory for generated files") - parser.add_argument("--validate", default=False, action='store_true', help="Only validate the provided configuration file against the schema and exit.") + parser.add_argument( + "--output-dir", "-o", default=".", help="Output directory for generated files" + ) + parser.add_argument( + "--validate", + default=False, + action="store_true", + help="Only validate the provided configuration file against the schema and exit.", + ) args = parser.parse_args() input_config = load_json_file(args.filename) @@ -1025,8 +1147,9 @@ def main(): preprocessed_config = preprocess_defaults(score_defaults, input_config) gen_health_monitor_config(args.output_dir, preprocessed_config) gen_launch_manager_config(args.output_dir, preprocessed_config) - + return 0 + if __name__ == "__main__": exit(main()) diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py index 16d33e8b..037478fe 100644 --- a/scripts/config_mapping/unit_tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -3,12 +3,13 @@ from lifecycle_config import preprocess_defaults import json + def test_preprocessing_basic(): """ Basic smoketest for the preprocess_defaults function, to ensure that defaults are being applied and overridden correctly. """ - global_defaults = json.loads(''' + global_defaults = json.loads(""" { "deployment_config": { "ready_timeout": 0.5, @@ -32,9 +33,9 @@ def test_preprocessing_basic(): "evaluation_cycle": 0.5 }, "watchdogs": {} - }''') + }""") - config = json.loads('''{ + config = json.loads("""{ "defaults": { "deployment_config": { "shutdown_timeout": 1.0, @@ -89,11 +90,11 @@ def test_preprocessing_basic(): "require_magic_close": false } } - }''') + }""") preprocessed_config = preprocess_defaults(global_defaults, config) - expected_config=json.loads('''{ + expected_config = json.loads("""{ "components": { "test_comp": { "description": "Test component", @@ -137,9 +138,11 @@ def test_preprocessing_basic(): "require_magic_close": false } } - }''') + }""") print("Dumping preprocessed configuration:") print(json.dumps(preprocessed_config, indent=4)) - assert preprocessed_config == expected_config, "Preprocessed config does not match expected config." + assert preprocessed_config == expected_config, ( + "Preprocessed config does not match expected config." + ) From 44a5c4e4e98c385cd2d5506b6487e71b51907df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 23 Feb 2026 11:05:00 +0100 Subject: [PATCH 36/72] Adapt expected output to file renaming --- .../{someip-daemon.json => hmproc_someip-daemon.json} | 0 .../{state_manager.json => hmproc_state_manager.json} | 0 .../expected_output/{test_app1.json => hmproc_test_app1.json} | 0 ..._component.json => hmproc_reporting_supervised_component.json} | 0 ...c_reporting_supervised_component_with_no_max_indications.json} | 0 .../{state_manager.json => hmproc_state_manager.json} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename scripts/config_mapping/tests/basic_test/expected_output/{someip-daemon.json => hmproc_someip-daemon.json} (100%) rename scripts/config_mapping/tests/basic_test/expected_output/{state_manager.json => hmproc_state_manager.json} (100%) rename scripts/config_mapping/tests/basic_test/expected_output/{test_app1.json => hmproc_test_app1.json} (100%) rename scripts/config_mapping/tests/health_config_test/expected_output/{reporting_supervised_component.json => hmproc_reporting_supervised_component.json} (100%) rename scripts/config_mapping/tests/health_config_test/expected_output/{reporting_supervised_component_with_no_max_indications.json => hmproc_reporting_supervised_component_with_no_max_indications.json} (100%) rename scripts/config_mapping/tests/health_config_test/expected_output/{state_manager.json => hmproc_state_manager.json} (100%) diff --git a/scripts/config_mapping/tests/basic_test/expected_output/someip-daemon.json b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_someip-daemon.json similarity index 100% rename from scripts/config_mapping/tests/basic_test/expected_output/someip-daemon.json rename to scripts/config_mapping/tests/basic_test/expected_output/hmproc_someip-daemon.json diff --git a/scripts/config_mapping/tests/basic_test/expected_output/state_manager.json b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_state_manager.json similarity index 100% rename from scripts/config_mapping/tests/basic_test/expected_output/state_manager.json rename to scripts/config_mapping/tests/basic_test/expected_output/hmproc_state_manager.json diff --git a/scripts/config_mapping/tests/basic_test/expected_output/test_app1.json b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_test_app1.json similarity index 100% rename from scripts/config_mapping/tests/basic_test/expected_output/test_app1.json rename to scripts/config_mapping/tests/basic_test/expected_output/hmproc_test_app1.json diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component.json similarity index 100% rename from scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component.json rename to scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component.json diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component_with_no_max_indications.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component_with_no_max_indications.json similarity index 100% rename from scripts/config_mapping/tests/health_config_test/expected_output/reporting_supervised_component_with_no_max_indications.json rename to scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component_with_no_max_indications.json diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/state_manager.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_state_manager.json similarity index 100% rename from scripts/config_mapping/tests/health_config_test/expected_output/state_manager.json rename to scripts/config_mapping/tests/health_config_test/expected_output/hmproc_state_manager.json From 5969a52a4269195de279eacd7b1199cd9e91654b Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 12:20:41 +0100 Subject: [PATCH 37/72] Make shortName required --- .../health_monitor_lib/config/hm_flatcfg.fbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs b/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs index 12ac79ca..ba3f5acd 100644 --- a/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs +++ b/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs @@ -149,7 +149,7 @@ table HmRefProcessGroupStatesGlobal { } table RecoveryNotification { - shortName: string (id:0); + shortName: string (id:0, required); recoveryNotificationTimeout: double (id:1); processGroupMetaModelIdentifier: string (id:2); refGlobalSupervisionIndex: uint32 (id:3); From 411402a4fa580cb592b4cb1508659442b414d3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 23 Feb 2026 11:12:44 +0100 Subject: [PATCH 38/72] Update Readme file with bazel interface --- scripts/config_mapping/BUILD | 14 +++++++------- scripts/config_mapping/Readme.md | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD index ff35ec38..17e0aea2 100644 --- a/scripts/config_mapping/BUILD +++ b/scripts/config_mapping/BUILD @@ -1,16 +1,16 @@ -load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") load("@score_lifecycle_pip//:requirements.bzl", "requirement") +# Example Usage +# load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") +# exports_files(["lm_config.json"]) +# gen_lifecycle_config( +# name ="example_config_gen", +# config="//scripts/config_mapping:lm_config.json") + py_binary( name = "lifecycle_config", srcs = ["lifecycle_config.py"], deps = [requirement("jsonschema")], visibility = ["//visibility:public"], ) - -# Example Usage -exports_files(["lm_config.json"]) -gen_lifecycle_config( - name ="example_config_gen", - config="//scripts/config_mapping:lm_config.json") diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index 77a330a4..c42d4009 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -8,6 +8,25 @@ Once the source code of the launch_manager has been adapted to read in the new c Providing a json file using the new configuration format as input, the script will first validate the configuration against its schema. Then it will map the content to the old configuration file format and generate those files into the specified output_dir. +## Bazel + +The bazel function `gen_lifecycle_config` handles the translation of the new configuration format into the old configuration format and also does the subsequent compilation to flatbuffer files. + +```python +load("@score_lifecycle_health//scripts/config_mapping:config.bzl", "gen_lifecycle_config") + +# This is your launch manager configuration in the new format +exports_files(["lm_config.json"]) + +# Afterwards, you can refer to the generated flatbuffer files with :example_config_gen +gen_lifecycle_config( + name ="example_config_gen", + config="//scripts/config_mapping:lm_config.json" +) +``` + +## Python + ``` python3 lifecycle_config.py -o ``` From 286131407594b76bcd01304a6dc7bb51b876470f Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 12:44:48 +0100 Subject: [PATCH 39/72] Move script to tests folder --- examples/config/lifecycle_demo.json | 166 ++++++++++++++++++ examples/run.sh | 4 +- .../scripts}/gen_lifecycle_config.py | 0 3 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 examples/config/lifecycle_demo.json rename {examples/config => tests/scripts}/gen_lifecycle_config.py (100%) diff --git a/examples/config/lifecycle_demo.json b/examples/config/lifecycle_demo.json new file mode 100644 index 00000000..f5e221c9 --- /dev/null +++ b/examples/config/lifecycle_demo.json @@ -0,0 +1,166 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/opt", + "ready_timeout": 2.0, + "shutdown_timeout": 2.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_daemon": { + "component_properties": { + "binary_name": "control_app/control_daemon", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_daemon" + } + } + }, + "demo_app0": { + "component_properties": { + "binary_name": "supervision_demo/cpp_supervised_app", + "application_profile": { + "application_type": "Reporting_And_Supervised" + }, + "process_arguments": [ + "-d50" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "demo_app0", + "CONFIG_PATH": "/opt/supervision_demo/etc/demo_app0.bin", + "IDENTIFIER": "demo_app0" + } + } + }, + "demo_app1": { + "component_properties": { + "binary_name": "supervision_demo/rust_supervised_app", + "application_profile": { + "application_type": "Reporting_And_Supervised" + }, + "process_arguments": [ + "-d50" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "demo_app1", + "CONFIG_PATH": "/opt/supervision_demo/etc/demo_app1.bin", + "IDENTIFIER": "demo_app1" + } + } + }, + "lifecycle_app0": { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app" + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "lifecycle_app0" + } + } + }, + "fallback_app": { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app", + "process_arguments": [ + "-v" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "fallback_app" + } + } + } + }, + "run_targets": { + "Off": { + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "Startup": { + "depends_on": [ + "control_daemon" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "Running": { + "depends_on": [ + "control_daemon", + "demo_app0", + "demo_app1", + "lifecycle_app0" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + } + }, + "initial_run_target": "Off", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [ + "control_daemon", + "fallback_app" + ] + } +} \ No newline at end of file diff --git a/examples/run.sh b/examples/run.sh index 6b51615e..83e0400d 100755 --- a/examples/run.sh +++ b/examples/run.sh @@ -45,9 +45,7 @@ rm -rf tmp rm -rf config/tmp mkdir config/tmp -python3 config/gen_lifecycle_config.py -c "$NUMBER_OF_CPP_PROCESSES" -r "$NUMBER_OF_RUST_PROCESSES" -n "$NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES" -o config/tmp/ - -python3 ../scripts/config_mapping/lifecycle_config.py config/tmp/lifecycle_demo.json -o config/tmp/ +python3 ../scripts/config_mapping/lifecycle_config.py config/lifecycle_demo.json -o config/tmp/ for f in config/tmp/*.json; do base=$(basename "$f") diff --git a/examples/config/gen_lifecycle_config.py b/tests/scripts/gen_lifecycle_config.py similarity index 100% rename from examples/config/gen_lifecycle_config.py rename to tests/scripts/gen_lifecycle_config.py From 0df67b7c356f982bda65f54992e52c14b42a200d Mon Sep 17 00:00:00 2001 From: SimonKozik <244535158+SimonKozik@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:45:13 +0000 Subject: [PATCH 40/72] Initial version of the new API --- .../include/score/lcm/control_client.h | 78 ++++--------------- 1 file changed, 17 insertions(+), 61 deletions(-) diff --git a/src/control_client_lib/include/score/lcm/control_client.h b/src/control_client_lib/include/score/lcm/control_client.h index f7a22df0..130a3c37 100644 --- a/src/control_client_lib/include/score/lcm/control_client.h +++ b/src/control_client_lib/include/score/lcm/control_client.h @@ -13,14 +13,11 @@ #ifndef CONTROL_CLIENT_H_ #define CONTROL_CLIENT_H_ -#include -#include - #include "score/concurrency/future/interruptible_future.h" #include "score/concurrency/future/interruptible_promise.h" #include "score/result/result.h" +#include #include "score/lcm/exec_error_domain.h" -#include "score/lcm/execution_error_event.h" namespace score { @@ -28,22 +25,14 @@ namespace lcm { class ControlClientImpl; -/// @brief Class representing connection to Launch Manager that is used to request Process Group state transitions (or other operations). +/// @brief Class representing connection to Launch Manager that is used to request Run Target activation (or other operations). /// @note ControlClient opens communication channel to Launch Manager (e.g. POSIX FIFO). Each Process that intends to perform state management, shall create an instance of this class and it shall have rights to use it. /// class ControlClient final { public: /// @brief Constructor that creates Control Client instance. /// - /// Registers given callback which is called in case a Process Group changes its state unexpectedly to an Undefined - /// Process Group State. - /// - /// @param[in] undefinedStateCallback callback to be invoked by ControlClient library if a ProcessGroup changes its - /// state unexpectedly to an Undefined Process Group State, i.e. without - /// previous request by SetState(). The affected ProcessGroup and ExecutionError - /// is provided as an argument to the callback in form of ExecutionErrorEvent. - /// - explicit ControlClient(std::function undefinedStateCallback) noexcept; + explicit ControlClient() noexcept; /// @brief Destructor of the Control Client instance. /// @param None. @@ -70,60 +59,27 @@ class ControlClient final { /// @returns the new reference ControlClient& operator=(ControlClient&& rval) noexcept; - /// @brief Method to request state transition for a single Process Group. - /// - /// This method will request Launch Manager to perform state transition and return immediately. - /// Returned InterruptibleFuture can be used to determine result of requested transition. - /// - /// @param[in] pg_name representing meta-model definition of a specific Process Group - /// @param[in] pg_state representing meta-model definition of a state. Launch Manager will perform state transition from the current state to the state identified by this parameter. - /// @returns void if requested transition is successful, otherwise it returns ExecErrorDomain error. - /// @error score::lcm::ExecErrc::kCancelled if transition to the requested Process Group state was cancelled by a newer request - /// @error score::lcm::ExecErrc::kFailed if transition to the requested Process Group state failed - /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnExit if Unexpected Termination in Process of previous Process Group State happened. - /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnEnter if Unexpected Termination in Process of target Process Group State happened. - /// @error score::lcm::ExecErrc::kInvalidArguments if arguments passed doesn't appear to be valid (e.g. after a software update, given processGroup doesn't exist anymore) - /// @error score::lcm::ExecErrc::kCommunicationError if ControlClient can't communicate with Launch Manager (e.g. IPC link is down) - /// @error score::lcm::ExecErrc::kAlreadyInState if the ProcessGroup is already in the requested state - /// @error score::lcm::ExecErrc::kInTransitionToSameState if a transition to the requested state is already ongoing - /// @error score::lcm::ExecErrc::kInvalidTransition if transition to the requested state is prohibited (e.g. Off state for MainPG) - /// @error score::lcm::ExecErrc::kGeneralError if any other error occurs. - /// - /// @threadsafety{thread-safe} - /// - score::concurrency::InterruptibleFuture SetState(const IdentifierHash& pg_name, const IdentifierHash& pg_state) const noexcept; - - /// @brief Method to retrieve result of Machine State initial transition to Startup state. + /// @brief Method to request activation of a specific Run Target. /// - /// This method allows State Management to retrieve result of a transition. - /// Please note that this transition happens once per machine life cycle, thus result delivered by this method shall not change (unless machine is started again). + /// This method will request Launch Manager to activate a Run Target and return immediately. + /// Returned InterruptibleFuture can be used to determine result of the activation request. /// - /// @param None. - /// @returns void if requested transition is successful, otherwise it returns ExecErrorDomain error. - /// @error score::lcm::ExecErrc::kCancelled if transition to the requested Process Group state was cancelled by a newer request - /// @error score::lcm::ExecErrc::kFailed if transition to the requested Process Group state failed + /// @param[in] runTargetName name of the Run Target that should be activated. Launch Manager will deactivate the currently active Run Target and activate Run Target identified by this parameter. + /// @returns void if activation requested is successful, otherwise it returns ExecErrorDomain error. + /// @error score::lcm::ExecErrc::kCancelled if activation requested was cancelled by a newer request + /// @error score::lcm::ExecErrc::kFailed if activation requested failed + /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnExit if Unexpected Termination of a Process assigned to the previously active Run Target happened. + /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnEnter if Unexpected Termination of a Process assigned the requested Run Target happened. + /// @error score::lcm::ExecErrc::kInvalidArguments if argument passed doesn't appear to be valid (e.g. after a software update, given Run Target doesn't exist anymore) /// @error score::lcm::ExecErrc::kCommunicationError if ControlClient can't communicate with Launch Manager (e.g. IPC link is down) + /// @error score::lcm::ExecErrc::kAlreadyInState if the requested Run Target is already active + /// @error score::lcm::ExecErrc::kInTransitionToSameState if there is already an ongoing request to activate requested Run Target + /// @error score::lcm::ExecErrc::kInvalidTransition if activation of the requested Run Target is prohibited (e.g. Off Run Target) /// @error score::lcm::ExecErrc::kGeneralError if any other error occurs. /// /// @threadsafety{thread-safe} /// - score::concurrency::InterruptibleFuture GetInitialMachineStateTransitionResult() const noexcept; - - /// @brief Returns the execution error which changed the given Process Group to an Undefined Process Group State. - /// - /// This function will return with error and will not return an ExecutionErrorEvent object, if the given - /// Process Group is in a defined Process Group state again. - /// - /// @param[in] processGroup Process Group of interest. - /// - /// @returns The execution error which changed the given Process Group to an Undefined Process Group State. - /// @error score::lcm::ExecErrc::kFailed Given Process Group is not in an Undefined Process Group State. - /// @error score::lcm::ExecErrc::kCommunicationError if ControlClient can't communicate with Launch Manager (e.g. IPC link is down) - /// - /// @threadsafety{thread-safe} - /// - score::Result GetExecutionError( - const IdentifierHash& processGroup) noexcept; + score::concurrency::InterruptibleFuture ActivateRunTarget(std::string_view runTargetName) const noexcept; private: /// @brief Pointer to implementation (Pimpl), we use this pattern to provide ABI compatibility. From 00f027b254b3dd9ee73eb127fc81de16de76f6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 23 Feb 2026 11:16:02 +0100 Subject: [PATCH 41/72] Remove flatbuffer script --- scripts/config_mapping/flatbuffer_rules.bzl | 77 --------------------- 1 file changed, 77 deletions(-) delete mode 100644 scripts/config_mapping/flatbuffer_rules.bzl diff --git a/scripts/config_mapping/flatbuffer_rules.bzl b/scripts/config_mapping/flatbuffer_rules.bzl deleted file mode 100644 index 366d96ef..00000000 --- a/scripts/config_mapping/flatbuffer_rules.bzl +++ /dev/null @@ -1,77 +0,0 @@ -def _flatbuffer_json_to_bin_impl(ctx): - flatc = ctx.executable.flatc - json = ctx.file.json - schema = ctx.file.schema - - # flatc will name the file the same as the json (can't be changed) - out_name = json.basename[:-len(".json")] + ".bin" - out = ctx.actions.declare_file(out_name, sibling = json) - - # flatc args --------------------------------- - flatc_args = [ - "-b", - "-o", - out.dirname, - ] - - for inc in ctx.attr.includes: - flatc_args.extend(["-I", inc.path]) - - if ctx.attr.strict_json: - flatc_args.append("--strict-json") - - flatc_args.extend([schema.path, json.path]) - # -------------------------------------------- - - ctx.actions.run( - inputs = [json, schema] + list(ctx.files.includes), - outputs = [out], - executable = flatc, - arguments = flatc_args, - progress_message = "flatc generation {}".format(json.short_path), - mnemonic = "FlatcGeneration", - ) - - rf = ctx.runfiles( - files = [out], - root_symlinks = { - ("_main/" + ctx.attr.out_dir + "/" + out_name): out, - }, - ) - - return DefaultInfo(files = depset([out]), runfiles = rf) - -flatbuffer_json_to_bin = rule( - implementation = _flatbuffer_json_to_bin_impl, - attrs = { - "json": attr.label( - allow_single_file = [".json"], - mandatory = True, - doc = "Json file to convert. Note that the binary file will have the same name as the json (minus the suffix)", - ), - "schema": attr.label( - allow_single_file = [".fbs"], - mandatory = True, - doc = "FBS file to use", - ), - "out_dir": attr.string( - default = "etc", - doc = "Directory to copy the generated file to, sibling to 'src' and 'tests' dirs. Do not include a trailing '/'", - ), - "flatc": attr.label( - default = Label("@flatbuffers//:flatc"), - executable = True, - cfg = "exec", - doc = "Reference to the flatc binary", - ), - # flatc arguments - "includes": attr.label_list( - allow_files = True, - doc = "Flatc include paths", - ), - "strict_json": attr.bool( - default = False, - doc = "Require strict JSON (no trailing commas etc)", - ), - }, -) From c0d63ba844dd630bf14988673e030f2ee796be1d Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Tue, 24 Feb 2026 15:14:38 +0100 Subject: [PATCH 42/72] Address reviewer comments --- examples/config/lifecycle_demo.json | 42 +++++++++++---------------- examples/demo.sh | 8 ++--- examples/run.sh | 2 +- tests/scripts/gen_lifecycle_config.py | 13 +++------ 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/examples/config/lifecycle_demo.json b/examples/config/lifecycle_demo.json index f5e221c9..fdd3b3ea 100644 --- a/examples/config/lifecycle_demo.json +++ b/examples/config/lifecycle_demo.json @@ -12,7 +12,7 @@ }, "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "Startup" } }, "environmental_variables": { @@ -60,7 +60,7 @@ } } }, - "demo_app0": { + "cpp_supervised_app": { "component_properties": { "binary_name": "supervision_demo/cpp_supervised_app", "application_profile": { @@ -72,13 +72,13 @@ }, "deployment_config": { "environmental_variables": { - "PROCESSIDENTIFIER": "demo_app0", - "CONFIG_PATH": "/opt/supervision_demo/etc/demo_app0.bin", - "IDENTIFIER": "demo_app0" + "PROCESSIDENTIFIER": "cpp_supervised_app", + "CONFIG_PATH": "/opt/supervision_demo/etc/cpp_supervised_app.bin", + "IDENTIFIER": "cpp_supervised_app" } } }, - "demo_app1": { + "rust_supervised_app": { "component_properties": { "binary_name": "supervision_demo/rust_supervised_app", "application_profile": { @@ -90,19 +90,19 @@ }, "deployment_config": { "environmental_variables": { - "PROCESSIDENTIFIER": "demo_app1", - "CONFIG_PATH": "/opt/supervision_demo/etc/demo_app1.bin", - "IDENTIFIER": "demo_app1" + "PROCESSIDENTIFIER": "rust_supervised_app", + "CONFIG_PATH": "/opt/supervision_demo/etc/rust_supervised_app.bin", + "IDENTIFIER": "rust_supervised_app" } } }, - "lifecycle_app0": { + "lifecycle_app": { "component_properties": { "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app" }, "deployment_config": { "environmental_variables": { - "PROCESSIDENTIFIER": "lifecycle_app0" + "PROCESSIDENTIFIER": "lifecycle_app" } } }, @@ -121,39 +121,31 @@ } }, "run_targets": { - "Off": { - "depends_on": [], - "recovery_action": { - "switch_run_target": { - "run_target": "Off" - } - } - }, "Startup": { "depends_on": [ "control_daemon" ], "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "Startup" } } }, "Running": { "depends_on": [ "control_daemon", - "demo_app0", - "demo_app1", - "lifecycle_app0" + "cpp_supervised_app", + "rust_supervised_app", + "lifecycle_app" ], "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "Startup" } } } }, - "initial_run_target": "Off", + "initial_run_target": "Startup", "alive_supervision": { "evaluation_cycle": 0.05 }, diff --git a/examples/demo.sh b/examples/demo.sh index ed4d4605..1d00227e 100755 --- a/examples/demo.sh +++ b/examples/demo.sh @@ -44,8 +44,8 @@ read -p "$(echo -e ${COLOR}Next: Killing an application process${NC})" echo "$> lmcontrol MainPG/Running" lmcontrol MainPG/Running sleep 2 -echo "$> pkill -9 demo_app0" -pkill -9 demo_app0 +echo "$> pkill -9 cpp_supervised" +pkill -9 cpp_supervised read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a" ps -a @@ -57,8 +57,8 @@ echo "$> lmcontrol MainPG/Running" lmcontrol MainPG/Running read -p "$(echo -e ${COLOR}Next: Trigger supervision failure${NC})" -echo "$> fail $(pgrep demo_app0)" -kill -s SIGUSR1 $(pgrep demo_app0) +echo "$> fail $(pgrep cpp_supervised)" +kill -s SIGUSR1 $(pgrep cpp_supervised) read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a" diff --git a/examples/run.sh b/examples/run.sh index 83e0400d..2dd2cce8 100755 --- a/examples/run.sh +++ b/examples/run.sh @@ -68,7 +68,7 @@ cp config/tmp/hmcore.bin tmp/launch_manager/etc/ mkdir -p tmp/supervision_demo/etc cp $DEMO_APP_BINARY tmp/supervision_demo/ -cp config/tmp/demo_app*.bin tmp/supervision_demo/etc/ +cp config/tmp/*app.bin tmp/supervision_demo/etc/ mkdir -p tmp/cpp_lifecycle_app/etc cp $DEMO_APP_WO_HM_BINARY tmp/cpp_lifecycle_app/ diff --git a/tests/scripts/gen_lifecycle_config.py b/tests/scripts/gen_lifecycle_config.py index 97e34c32..526692a0 100644 --- a/tests/scripts/gen_lifecycle_config.py +++ b/tests/scripts/gen_lifecycle_config.py @@ -41,7 +41,7 @@ def gen_lifecycle_config( "ready_timeout": 2.0, "shutdown_timeout": 2.0, "ready_recovery_action": {"restart": {"number_of_attempts": 0}}, - "recovery_action": {"switch_run_target": {"run_target": "Off"}}, + "recovery_action": {"switch_run_target": {"run_target": "Startup"}}, "environmental_variables": {"LD_LIBRARY_PATH": "/opt/lib"}, "sandbox": { "uid": 0, @@ -66,7 +66,7 @@ def gen_lifecycle_config( }, "components": {}, "run_targets": {}, - "initial_run_target": "Off", + "initial_run_target": "Startup", "alive_supervision": {"evaluation_cycle": 0.05}, } @@ -146,19 +146,14 @@ def gen_lifecycle_config( } # --- Run targets --- - config["run_targets"]["Off"] = { - "depends_on": [], - "recovery_action": {"switch_run_target": {"run_target": "Off"}}, - } - config["run_targets"]["Startup"] = { "depends_on": ["control_daemon"], - "recovery_action": {"switch_run_target": {"run_target": "Off"}}, + "recovery_action": {"switch_run_target": {"run_target": "Startup"}}, } config["run_targets"]["Running"] = { "depends_on": running_deps, - "recovery_action": {"switch_run_target": {"run_target": "Off"}}, + "recovery_action": {"switch_run_target": {"run_target": "Startup"}}, } # Fallback run target: control daemon + verbose app run during recovery From 196527b6055779af49f3e0dbe8f718a9c961e62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 24 Feb 2026 07:35:31 +0100 Subject: [PATCH 43/72] Adapt ControlClient implementation to new API --- examples/control_application/control.hpp | 4 +-- .../control_application/control_app_cli.cpp | 10 +++--- .../control_application/control_daemon.cpp | 24 +++++-------- src/control_client_lib/BUILD | 2 +- .../include/score/lcm/control_client.h | 1 + src/control_client_lib/src/control_client.cpp | 35 +++---------------- .../src/control_client_impl.cpp | 1 - .../src/control_client_impl.hpp | 1 + .../score/lcm => src}/execution_error_event.h | 0 9 files changed, 24 insertions(+), 54 deletions(-) rename src/control_client_lib/{include/score/lcm => src}/execution_error_event.h (100%) diff --git a/examples/control_application/control.hpp b/examples/control_application/control.hpp index 1cf72686..3e379b05 100644 --- a/examples/control_application/control.hpp +++ b/examples/control_application/control.hpp @@ -15,8 +15,8 @@ #include -struct ProcessGroupInfo { - char processGroupStatePath[1024]{}; +struct RunTargetInfo { + char runTargetName[1024]{}; }; static constexpr char const* control_socket_path = "/sm_control"; diff --git a/examples/control_application/control_app_cli.cpp b/examples/control_application/control_app_cli.cpp index d6a79557..ac93161b 100644 --- a/examples/control_application/control_app_cli.cpp +++ b/examples/control_application/control_app_cli.cpp @@ -18,19 +18,19 @@ int main(int argc, char** argv) { if(argc <= 1) { - std::cout << "Usage: " << argv[0] << " /My/ProcessGroup/State"; + std::cout << "Usage: " << argv[0] << " MyRunTargetName" << std::endl; return EXIT_FAILURE; } - ipc_dropin::Socket(sizeof(ProcessGroupInfo)), control_socket_capacity> sm_control_socket{}; + ipc_dropin::Socket(sizeof(RunTargetInfo)), control_socket_capacity> sm_control_socket{}; if (sm_control_socket.connect(control_socket_path) != ipc_dropin::ReturnCode::kOk) { std::cerr << "Could not connect to control socket" << std::endl; return EXIT_FAILURE; } - ProcessGroupInfo pg{}; - std::strncpy(pg.processGroupStatePath, argv[1], sizeof(pg.processGroupStatePath) - 1); - if(ipc_dropin::ReturnCode::kOk == sm_control_socket.trySend(pg)) { + RunTargetInfo info{}; + std::strncpy(info.runTargetName, argv[1], sizeof(info.runTargetName) - 1); + if(ipc_dropin::ReturnCode::kOk == sm_control_socket.trySend(info)) { std::cout << "Successfully sent request" << std::endl; return EXIT_SUCCESS; } else { diff --git a/examples/control_application/control_daemon.cpp b/examples/control_application/control_daemon.cpp index 5cd0533e..f0d4b87f 100644 --- a/examples/control_application/control_daemon.cpp +++ b/examples/control_application/control_daemon.cpp @@ -18,7 +18,6 @@ #include #include -#include #include "ipc_dropin/socket.hpp" #include "control.hpp" @@ -33,31 +32,26 @@ int main(int argc, char** argv) { score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); - ipc_dropin::Socket(sizeof(ProcessGroupInfo)), control_socket_capacity> sm_control_socket{}; + ipc_dropin::Socket(sizeof(RunTargetInfo)), control_socket_capacity> sm_control_socket{}; if (sm_control_socket.create(control_socket_path, 600) != ipc_dropin::ReturnCode::kOk) { std::cerr << "Could not create control socket" << std::endl; return EXIT_FAILURE; } - score::lcm::ControlClient client([](const score::lcm::ExecutionErrorEvent& event) { - std::cerr << "Undefined state callback invoked for process group id: " << event.processGroup << std::endl; - }); + score::lcm::ControlClient client; score::safecpp::Scope<> scope{}; while (!exitRequested) { - ProcessGroupInfo pgInfo{}; - if (ipc_dropin::ReturnCode::kOk == sm_control_socket.tryReceive(pgInfo)) { + RunTargetInfo info{}; + if (ipc_dropin::ReturnCode::kOk == sm_control_socket.tryReceive(info)) { - std::string targetProcessGroupState{pgInfo.processGroupStatePath}; - std::string targetProcessGroup = targetProcessGroupState.substr(0, targetProcessGroupState.find_last_of('/')); - - const score::lcm::IdentifierHash processGroup{targetProcessGroup}; - const score::lcm::IdentifierHash processGroupState{targetProcessGroupState}; - client.SetState(processGroup, processGroupState).Then({scope, [targetProcessGroupState](auto& result) noexcept { + std::string runTargetName{info.runTargetName}; + std::cout << "Activating Run Target: " << runTargetName << std::endl; + client.ActivateRunTarget(runTargetName).Then({scope, [runTargetName](auto& result) noexcept { if (!result) { - std::cerr << "Setting ProcessGroup state " << targetProcessGroupState << " failed with error: " << result.error().Message() << std::endl; + std::cerr << "Activating Run Target " << runTargetName << " failed with error: " << result.error().Message() << std::endl; } else { - std::cout << "Setting ProcessGroup state " << targetProcessGroupState << " succeeded" << std::endl; + std::cout << "Activating Run Target " << runTargetName << " succeeded" << std::endl; } }}); } diff --git a/src/control_client_lib/BUILD b/src/control_client_lib/BUILD index 3d776e5a..d119de27 100644 --- a/src/control_client_lib/BUILD +++ b/src/control_client_lib/BUILD @@ -17,10 +17,10 @@ cc_library( "src/control_client.cpp", "src/control_client_impl.cpp", "src/control_client_impl.hpp", + "src/execution_error_event.h", ], hdrs = [ "include/score/lcm/control_client.h", - "include/score/lcm/execution_error_event.h", ], includes = ["include"], visibility = ["//visibility:public"], diff --git a/src/control_client_lib/include/score/lcm/control_client.h b/src/control_client_lib/include/score/lcm/control_client.h index 130a3c37..020ed5f9 100644 --- a/src/control_client_lib/include/score/lcm/control_client.h +++ b/src/control_client_lib/include/score/lcm/control_client.h @@ -17,6 +17,7 @@ #include "score/concurrency/future/interruptible_promise.h" #include "score/result/result.h" #include +#include #include "score/lcm/exec_error_domain.h" namespace score { diff --git a/src/control_client_lib/src/control_client.cpp b/src/control_client_lib/src/control_client.cpp index 179a51c3..c20d9f42 100644 --- a/src/control_client_lib/src/control_client.cpp +++ b/src/control_client_lib/src/control_client.cpp @@ -30,7 +30,8 @@ inline score::concurrency::InterruptibleFuture GetErrorFuture(score::lcm:: return tmp_.GetInterruptibleFuture().value(); } -ControlClient::ControlClient(std::function undefinedStateCallback) noexcept { +ControlClient::ControlClient() noexcept { + static std::function undefinedStateCallback = [](const score::lcm::ExecutionErrorEvent& event) {}; try { control_client_impl_ = std::make_unique(undefinedStateCallback); } catch (...) { @@ -50,12 +51,14 @@ ControlClient::ControlClient(ControlClient&& rval) noexcept { ControlClient& ControlClient::operator=(ControlClient&& rval) noexcept = default; -score::concurrency::InterruptibleFuture ControlClient::SetState( const IdentifierHash& pg_name, const IdentifierHash& pg_state ) const noexcept +score::concurrency::InterruptibleFuture ControlClient::ActivateRunTarget(std::string_view runTargetName) const noexcept { score::concurrency::InterruptibleFuture retVal_ {}; if( control_client_impl_ != nullptr ) { + static IdentifierHash pg_name{"MainPG"}; + IdentifierHash pg_state{"MainPG/" + std::string(runTargetName)}; retVal_ = control_client_impl_->SetState(pg_name, pg_state); } else @@ -66,34 +69,6 @@ score::concurrency::InterruptibleFuture ControlClient::SetState( const Ide return retVal_; } -score::concurrency::InterruptibleFuture ControlClient::GetInitialMachineStateTransitionResult() const noexcept { - score::concurrency::InterruptibleFuture retVal_{}; - - if( control_client_impl_ != nullptr ) - { - retVal_ = control_client_impl_->GetInitialMachineStateTransitionResult(); - } - else - { - retVal_ = GetErrorFuture(ExecErrc::kCommunicationError); - } - - return retVal_; -} - -score::Result ControlClient::GetExecutionError( - const score::lcm::IdentifierHash& processGroup ) noexcept -{ - score::Result retVal_ {score::MakeUnexpected(score::lcm::ExecErrc::kCommunicationError) }; - - if (control_client_impl_ != nullptr) { - retVal_ = control_client_impl_->GetExecutionError(processGroup); - } - // else not needed as kCommunicationError is the default return value - - return retVal_; -} - } // namespace lcm } // namespace score diff --git a/src/control_client_lib/src/control_client_impl.cpp b/src/control_client_lib/src/control_client_impl.cpp index 41fd6c80..804857ca 100644 --- a/src/control_client_lib/src/control_client_impl.cpp +++ b/src/control_client_lib/src/control_client_impl.cpp @@ -21,7 +21,6 @@ #include "score/concurrency/future/interruptible_future.h" #include "score/concurrency/future/interruptible_promise.h" -#include #include #include diff --git a/src/control_client_lib/src/control_client_impl.hpp b/src/control_client_lib/src/control_client_impl.hpp index 5a0e0080..d0a89b45 100644 --- a/src/control_client_lib/src/control_client_impl.hpp +++ b/src/control_client_lib/src/control_client_impl.hpp @@ -24,6 +24,7 @@ #include #include #include +#include "execution_error_event.h" namespace score { diff --git a/src/control_client_lib/include/score/lcm/execution_error_event.h b/src/control_client_lib/src/execution_error_event.h similarity index 100% rename from src/control_client_lib/include/score/lcm/execution_error_event.h rename to src/control_client_lib/src/execution_error_event.h From 0ce9867b692375257d2ed797ca6dac4fd2293629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 25 Feb 2026 07:52:00 +0100 Subject: [PATCH 44/72] Update health config integration test --- .../tests/basic_test/expected_output/hm_demo.json | 10 ++++++++++ .../health_config_test/expected_output/hm_demo.json | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json index 6d81447f..fd63bcc3 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json @@ -16,6 +16,9 @@ } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -35,6 +38,9 @@ } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -54,6 +60,9 @@ } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -231,6 +240,7 @@ ], "hmRecoveryNotification": [ { + "shortName": "recovery_notification", "recoveryNotificationTimeout": 5000, "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", "refGlobalSupervisionIndex": 0, diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json index 6fd37b80..db38da3d 100644 --- a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json @@ -16,6 +16,9 @@ } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -35,6 +38,9 @@ } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -54,6 +60,9 @@ } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -231,6 +240,7 @@ ], "hmRecoveryNotification": [ { + "shortName": "recovery_notification", "recoveryNotificationTimeout": 5000, "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", "refGlobalSupervisionIndex": 0, From 4c914323c98eeaf89de368fdee6e43af7cf5464d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 25 Feb 2026 15:09:16 +0100 Subject: [PATCH 45/72] Fix merge error in MODULE.bazel --- MODULE.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index 6855beee..414c45a9 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -114,7 +114,7 @@ python.toolchain( use_repo(python) # Python pip dependencies -pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True) pip.parse( hub_name = "score_lifecycle_pip", python_version = PYTHON_VERSION, From 0330b8b2c74eb1872ca0c3653d0ba28b3212d440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 25 Feb 2026 15:14:38 +0100 Subject: [PATCH 46/72] Adapt examples to use the bazel config generation --- examples/BUILD | 15 +++++++++++ examples/README.md | 10 +++----- examples/config/BUILD | 9 +++++++ examples/config/lifecycle_demo.json | 4 +-- examples/cpp_supervised_app/main.cpp | 4 +-- examples/demo.sh | 25 ++++++++++--------- examples/run.sh | 22 ++++------------ .../score/lcm/saf/recovery/Notification.cpp | 20 +++++++++++---- 8 files changed, 65 insertions(+), 44 deletions(-) create mode 100644 examples/BUILD create mode 100644 examples/config/BUILD diff --git a/examples/BUILD b/examples/BUILD new file mode 100644 index 00000000..e84422f2 --- /dev/null +++ b/examples/BUILD @@ -0,0 +1,15 @@ +load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") +gen_lifecycle_config( + name ="example_config", + config="//examples/config:lifecycle_demo_config") + +filegroup( + name = "example_apps", + srcs = [ + "//examples/control_application:control_daemon", + "//examples/control_application:lmcontrol", + "//examples/cpp_supervised_app:cpp_supervised_app", + "//examples/rust_supervised_app:rust_supervised_app", + "//examples:example_config" + ], +) diff --git a/examples/README.md b/examples/README.md index 6ffd56b6..dc266407 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,23 +2,21 @@ ## Building & Running the demo setup -1. Build launch_manager and health_monitor, in the parent folder, first. +1. Build launch_manager and examples first 2. Then start run.sh script in this folder: `cd demo && ./run.sh` The run.sh script will: -- Copy the required binaries to a temporary directory demo/tmp -- Compile the json configuration to flatbuffer using flatc - Build a docker image for execution with the required artifacts inside - Start the docker container that runs launch_manager ## Interacting with the Demo -### Changing ProcessGroup States +### Changing RunTargets -There is a CLI application that allows to request transition to a certain ProcessGroup State. +There is a CLI application that allows to request transition to a certain RunTarget. -Example: `lmcontrol ProcessGroup1/Startup` +Example: `lmcontrol Startup` ### Triggering Supervision Failure diff --git a/examples/config/BUILD b/examples/config/BUILD new file mode 100644 index 00000000..b786c706 --- /dev/null +++ b/examples/config/BUILD @@ -0,0 +1,9 @@ +exports_files( + ["lifecycle_demo.json"], + visibility = ["//examples:__subpackages__"]) + +filegroup( + name = "lifecycle_demo_config", + srcs = ["lifecycle_demo.json"], + visibility = ["//examples:__subpackages__"], +) diff --git a/examples/config/lifecycle_demo.json b/examples/config/lifecycle_demo.json index fdd3b3ea..4d12f412 100644 --- a/examples/config/lifecycle_demo.json +++ b/examples/config/lifecycle_demo.json @@ -73,7 +73,7 @@ "deployment_config": { "environmental_variables": { "PROCESSIDENTIFIER": "cpp_supervised_app", - "CONFIG_PATH": "/opt/supervision_demo/etc/cpp_supervised_app.bin", + "CONFIG_PATH": "/opt/supervision_demo/etc/hmproc_cpp_supervised_app.bin", "IDENTIFIER": "cpp_supervised_app" } } @@ -91,7 +91,7 @@ "deployment_config": { "environmental_variables": { "PROCESSIDENTIFIER": "rust_supervised_app", - "CONFIG_PATH": "/opt/supervision_demo/etc/rust_supervised_app.bin", + "CONFIG_PATH": "/opt/supervision_demo/etc/hmproc_rust_supervised_app.bin", "IDENTIFIER": "rust_supervised_app" } } diff --git a/examples/cpp_supervised_app/main.cpp b/examples/cpp_supervised_app/main.cpp index b9145ef3..0c615216 100644 --- a/examples/cpp_supervised_app/main.cpp +++ b/examples/cpp_supervised_app/main.cpp @@ -163,9 +163,9 @@ int main(int argc, char** argv) } } - if (stopReportingCheckpoints.load()) + while (stopReportingCheckpoints && !exitRequested) { - std::this_thread::sleep_for(std::chrono::milliseconds(5000)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return EXIT_SUCCESS; diff --git a/examples/demo.sh b/examples/demo.sh index 1d00227e..1e506cab 100755 --- a/examples/demo.sh +++ b/examples/demo.sh @@ -20,9 +20,9 @@ ps -a | head -n 40 echo "$> ps -a | wc -l" ps -a | wc -l -read -p "$(echo -e ${COLOR}Next: Turning on MainPG/Running${NC})" -echo "$> lmcontrol MainPG/Running" -lmcontrol MainPG/Running +read -p "$(echo -e ${COLOR}Next: Turning on RunTarget Running${NC})" +echo "$> lmcontrol Running" +lmcontrol Running read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a | wc -l" @@ -32,18 +32,19 @@ read -p "$(echo -e ${COLOR}Next: Show CPU utilization${NC})" echo "$> top" top -read -p "$(echo -e ${COLOR}Next: Turning off demo apps via MainPG/Startup${NC})" -echo "$> lmcontrol MainPG/Startup" -lmcontrol MainPG/Startup +read -p "$(echo -e ${COLOR}Next: Turning off demo apps via RunTarget Startup${NC})" +echo "$> lmcontrol Startup" +lmcontrol Startup read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a | wc -l" ps -a | wc -l +read -p "$(echo -e ${COLOR}Next: Transition back to RunTarget Running${NC})" +echo "$> lmcontrol Running" +lmcontrol Running + read -p "$(echo -e ${COLOR}Next: Killing an application process${NC})" -echo "$> lmcontrol MainPG/Running" -lmcontrol MainPG/Running -sleep 2 echo "$> pkill -9 cpp_supervised" pkill -9 cpp_supervised read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" @@ -52,9 +53,9 @@ ps -a echo "$> ps -a | wc -l" ps -a | wc -l -read -p "$(echo -e ${COLOR}Next: Moving back to MainPG/Running${NC})" -echo "$> lmcontrol MainPG/Running" -lmcontrol MainPG/Running +read -p "$(echo -e ${COLOR}Next: Moving back to RunTarget Running${NC})" +echo "$> lmcontrol Running" +lmcontrol Running read -p "$(echo -e ${COLOR}Next: Trigger supervision failure${NC})" echo "$> fail $(pgrep cpp_supervised)" diff --git a/examples/run.sh b/examples/run.sh index 2dd2cce8..3f572582 100755 --- a/examples/run.sh +++ b/examples/run.sh @@ -29,6 +29,7 @@ DEMO_APP_WO_HM_BINARY="$PWD/../bazel-bin/examples/cpp_lifecycle_app/cpp_lifecycl RUST_APP_BINARY="$PWD/../bazel-bin/examples/rust_supervised_app/rust_supervised_app" CONTROL_APP_BINARY="$PWD/../bazel-bin/examples/control_application/control_daemon" CONTROL_CLI_BINARY="$PWD/../bazel-bin/examples/control_application/lmcontrol" +CFG_DIR="$PWD/../bazel-bin/examples/flatbuffer_out/" file_exists $LM_BINARY file_exists $DEMO_APP_BINARY @@ -45,30 +46,17 @@ rm -rf tmp rm -rf config/tmp mkdir config/tmp -python3 ../scripts/config_mapping/lifecycle_config.py config/lifecycle_demo.json -o config/tmp/ - -for f in config/tmp/*.json; do - base=$(basename "$f") - if [[ "$base" != "lm_demo.json" && "$base" != "hmcore.json" && "$base" != "lifecycle_demo.json" ]]; then - ../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs "$f" - fi -done - -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/config/lm_flatcfg.fbs config/tmp/lm_demo.json - -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hmcore_flatcfg.fbs config/tmp/hmcore.json - mkdir -p tmp/launch_manager/etc cp $LM_BINARY tmp/launch_manager/launch_manager -cp config/tmp/lm_demo.bin tmp/launch_manager/etc/ +cp $CFG_DIR/lm_demo.bin tmp/launch_manager/etc/ cp config/lm_logging.json tmp/launch_manager/etc/logging.json -cp config/tmp/hm_demo.bin tmp/launch_manager/etc/ -cp config/tmp/hmcore.bin tmp/launch_manager/etc/ +cp $CFG_DIR/hm_demo.bin tmp/launch_manager/etc/ +cp $CFG_DIR/hmcore.bin tmp/launch_manager/etc/ mkdir -p tmp/supervision_demo/etc cp $DEMO_APP_BINARY tmp/supervision_demo/ -cp config/tmp/*app.bin tmp/supervision_demo/etc/ +cp $CFG_DIR/*_supervised_app.bin tmp/supervision_demo/etc/ mkdir -p tmp/cpp_lifecycle_app/etc cp $DEMO_APP_WO_HM_BINARY tmp/cpp_lifecycle_app/ diff --git a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp index c13fc8ef..62269859 100644 --- a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp +++ b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp @@ -155,12 +155,22 @@ void Notification::verifyRecoveryHandlerResponse(void) const auto result = recoveryStateFutureOutput.Get(score::cpp::stop_token{}); if (!result.has_value()) { - logger_r.LogWarn() << messageHeader << "Recovery state request returned with error:" << result.error().Message(); - logger_r.LogDebug() << messageHeader << "Incorrect response received from the Recovery state request call"; + // We may be already in the requested state, due to LM executing recovery on its own. + // This is e.g. the case if the process crashed and LM transitions to its recovery state before supervisions expire. + if (*result.error() == static_cast(score::lcm::ExecErrc::kAlreadyInState)) + { + logger_r.LogWarn() << messageHeader + << "Recovery state request returned:" << result.error().Message(); + } + else { + logger_r.LogWarn() << messageHeader + << "Recovery state request returned with error:" << result.error().Message(); + logger_r.LogDebug() << messageHeader << "Incorrect response received from the Recovery state request call"; - startTimestamp = 0U; - setFinalTimeoutState(); - return; + startTimestamp = 0U; + setFinalTimeoutState(); + return; + } } logger_r.LogDebug() << messageHeader << "Correct response received from the Recovery state request call"; From b483f37079e9d0cbb09594967339f1c4cbd75813 Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Mon, 2 Mar 2026 14:18:03 +0100 Subject: [PATCH 47/72] Start demo via bazel target --- examples/BUILD | 18 ++++++++++++++++++ examples/README.md | 5 ++--- examples/run.sh | 35 +++++++++++++++++++---------------- examples/run_examples.bzl | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 examples/run_examples.bzl diff --git a/examples/BUILD b/examples/BUILD index e84422f2..8f3c5064 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -1,4 +1,6 @@ load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") +load(":run_examples.bzl", "run_examples") + gen_lifecycle_config( name ="example_config", config="//examples/config:lifecycle_demo_config") @@ -8,8 +10,24 @@ filegroup( srcs = [ "//examples/control_application:control_daemon", "//examples/control_application:lmcontrol", + "//examples/cpp_lifecycle_app:cpp_lifecycle_app", "//examples/cpp_supervised_app:cpp_supervised_app", "//examples/rust_supervised_app:rust_supervised_app", "//examples:example_config" ], ) + +filegroup( + name = "lm_binaries", + srcs = [ + "//src/launch_manager_daemon:launch_manager", + "//src/launch_manager_daemon/process_state_client_lib:process_state_client", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//src/control_client_lib:control_client_lib", + ] +) + +run_examples( + name = "run_examples", + deps = [":example_apps", ":lm_binaries"], +) \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index dc266407..385ff981 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,8 +2,7 @@ ## Building & Running the demo setup -1. Build launch_manager and examples first -2. Then start run.sh script in this folder: `cd demo && ./run.sh` +Execute `bazel run //examples:run_examples --config=<...>`. This will build all dependences and run the run.sh script The run.sh script will: @@ -29,4 +28,4 @@ Example: `fail ` There is an interactive mode that walks you through two demo scenarios. This mode requires the run.sh script to be executed **in an active tmux** session. -`cd demo && ./run.sh tmux` +`bazel run //examples:run_examples --config=<...> -- tmux` diff --git a/examples/run.sh b/examples/run.sh index 3f572582..d6812609 100755 --- a/examples/run.sh +++ b/examples/run.sh @@ -23,13 +23,22 @@ file_exists() { fi } -LM_BINARY="$PWD/../bazel-bin/src/launch_manager_daemon/launch_manager" -DEMO_APP_BINARY="$PWD/../bazel-bin/examples/cpp_supervised_app/cpp_supervised_app" -DEMO_APP_WO_HM_BINARY="$PWD/../bazel-bin/examples/cpp_lifecycle_app/cpp_lifecycle_app" -RUST_APP_BINARY="$PWD/../bazel-bin/examples/rust_supervised_app/rust_supervised_app" -CONTROL_APP_BINARY="$PWD/../bazel-bin/examples/control_application/control_daemon" -CONTROL_CLI_BINARY="$PWD/../bazel-bin/examples/control_application/lmcontrol" -CFG_DIR="$PWD/../bazel-bin/examples/flatbuffer_out/" +# When run via 'bazel run', BUILD_WORKSPACE_DIRECTORY is set to the workspace root. +# Otherwise, assume we're running from the examples/ directory. +if [ -n "$BUILD_WORKSPACE_DIRECTORY" ]; then + BAZEL_BIN="$BUILD_WORKSPACE_DIRECTORY/bazel-bin" + cd "$BUILD_WORKSPACE_DIRECTORY/examples" +else + BAZEL_BIN="$PWD/../bazel-bin" +fi + +LM_BINARY="$BAZEL_BIN/src/launch_manager_daemon/launch_manager" +DEMO_APP_BINARY="$BAZEL_BIN/examples/cpp_supervised_app/cpp_supervised_app" +DEMO_APP_WO_HM_BINARY="$BAZEL_BIN/examples/cpp_lifecycle_app/cpp_lifecycle_app" +RUST_APP_BINARY="$BAZEL_BIN/examples/rust_supervised_app/rust_supervised_app" +CONTROL_APP_BINARY="$BAZEL_BIN/examples/control_application/control_daemon" +CONTROL_CLI_BINARY="$BAZEL_BIN/examples/control_application/lmcontrol" +CFG_DIR="$BAZEL_BIN/examples/flatbuffer_out/" file_exists $LM_BINARY file_exists $DEMO_APP_BINARY @@ -38,13 +47,7 @@ file_exists $RUST_APP_BINARY file_exists $CONTROL_APP_BINARY file_exists $CONTROL_CLI_BINARY -NUMBER_OF_CPP_PROCESSES=1 -NUMBER_OF_RUST_PROCESSES=1 -NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES=1 - rm -rf tmp -rm -rf config/tmp -mkdir config/tmp mkdir -p tmp/launch_manager/etc cp $LM_BINARY tmp/launch_manager/launch_manager @@ -68,9 +71,9 @@ cp $CONTROL_APP_BINARY tmp/control_app/ cp $CONTROL_CLI_BINARY tmp/control_app/ mkdir -p tmp/lib -cp $PWD/../bazel-bin/src/launch_manager_daemon/process_state_client_lib/libprocess_state_client.so tmp/lib/ -cp $PWD/../bazel-bin/src/launch_manager_daemon/lifecycle_client_lib/liblifecycle_client.so tmp/lib/ -cp $PWD/../bazel-bin/src/control_client_lib/libcontrol_client_lib.so tmp/lib/ +cp $BAZEL_BIN/src/launch_manager_daemon/process_state_client_lib/libprocess_state_client.so tmp/lib/ +cp $BAZEL_BIN/src/launch_manager_daemon/lifecycle_client_lib/liblifecycle_client.so tmp/lib/ +cp $BAZEL_BIN/src/control_client_lib/libcontrol_client_lib.so tmp/lib/ docker build . -t demo diff --git a/examples/run_examples.bzl b/examples/run_examples.bzl new file mode 100644 index 00000000..5eed488b --- /dev/null +++ b/examples/run_examples.bzl @@ -0,0 +1,34 @@ +def _impl_run_examples(ctx): + run_script = ctx.file._run_script + + launcher = ctx.actions.declare_file(ctx.label.name + "_launcher.sh") + ctx.actions.write( + output = launcher, + content = "#!/bin/bash\nexec {run_script} \"$@\"\n".format( + run_script = run_script.short_path, + ), + is_executable = True, + ) + + runfiles = ctx.runfiles(files = [run_script] + ctx.files.deps) + for dep in ctx.attr.deps: + runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) + + return DefaultInfo( + executable = launcher, + runfiles = runfiles, + ) + +run_examples = rule( + implementation = _impl_run_examples, + executable = True, + attrs = { + "deps": attr.label_list( + default = [":example_apps"], + ), + "_run_script": attr.label( + default = Label("//examples:run.sh"), + allow_single_file = True, + ), + }, +) \ No newline at end of file From 3821f213f853a74cc35fe5f293035d63dc32de24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 27 Feb 2026 10:32:28 +0100 Subject: [PATCH 48/72] Merge different pip environments --- requirements.in | 5 ++++- requirements_lock.txt | 21 +++++++++++++------ scripts/config_mapping/Readme.md | 2 +- .../config_mapping/requirements_external.txt | 1 - .../config_mapping/requirements_internal.txt | 3 --- tests/integration/BUILD | 2 +- tests/integration/smoke/BUILD | 2 +- 7 files changed, 22 insertions(+), 14 deletions(-) delete mode 100644 scripts/config_mapping/requirements_external.txt delete mode 100644 scripts/config_mapping/requirements_internal.txt diff --git a/requirements.in b/requirements.in index 45314edc..0e423780 100644 --- a/requirements.in +++ b/requirements.in @@ -1,2 +1,5 @@ # Python dependencies for generating lifecycle configuration files --r ./scripts/config_mapping/requirements_external.txt +-r ./scripts/config_mapping/requirements.txt + +-r ./tests/integration/requirements.txt + diff --git a/requirements_lock.txt b/requirements_lock.txt index 7d799e4a..48e2b989 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -1,6 +1,15 @@ -attrs==25.4.0 -jsonschema==4.26.0 -jsonschema-specifications==2025.9.1 -referencing==0.37.0 -rpds-py==0.30.0 -typing_extensions==4.15.0 \ No newline at end of file +attrs==25.3.0 +exceptiongroup==1.3.1 +importlib-resources==6.4.5 +iniconfig==2.1.0 +jsonschema==4.23.0 +jsonschema-specifications==2023.12.1 +packaging==26.0 +pkgutil-resolve-name==1.3.10 +pluggy==1.5.0 +pytest==8.3.5 +referencing==0.35.1 +rpds-py==0.20.1 +tomli==2.4.0 +typing-extensions==4.13.2 +zipp==3.20.2 diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index c42d4009..d4ac9be8 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -44,7 +44,7 @@ You may want to use the virtual environment: ```bash python3 -m venv myvenv . myvenv/bin/activate -pip3 install -r requirements_internal.txt +pip3 install -r requirements.txt ``` Execute tests: diff --git a/scripts/config_mapping/requirements_external.txt b/scripts/config_mapping/requirements_external.txt deleted file mode 100644 index 7b8f0158..00000000 --- a/scripts/config_mapping/requirements_external.txt +++ /dev/null @@ -1 +0,0 @@ -jsonschema \ No newline at end of file diff --git a/scripts/config_mapping/requirements_internal.txt b/scripts/config_mapping/requirements_internal.txt deleted file mode 100644 index 3d3b0be3..00000000 --- a/scripts/config_mapping/requirements_internal.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest>=7.0.0 -pytest-json-report>=1.5.0 -jsonschema diff --git a/tests/integration/BUILD b/tests/integration/BUILD index b2ba66a2..7c4b0659 100644 --- a/tests/integration/BUILD +++ b/tests/integration/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@pip_score_venv_test//:requirements.bzl", "all_requirements") +load("@score_lifecycle_pip//:requirements.bzl", "all_requirements") load("@rules_python//python:pip.bzl", "compile_pip_requirements") load("@score_tooling//python_basics:defs.bzl", "score_py_pytest", "score_virtualenv") diff --git a/tests/integration/smoke/BUILD b/tests/integration/smoke/BUILD index 1deb9a9f..86ea1591 100644 --- a/tests/integration/smoke/BUILD +++ b/tests/integration/smoke/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@pip_score_venv_test//:requirements.bzl", "all_requirements") +load("@score_lifecycle_pip//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") load("//config:flatbuffers_rules.bzl", "flatbuffer_json_to_bin") From 35d7581dd507f2fb887dc0cd2de2a2d1d8cf083c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 3 Mar 2026 08:51:27 +0100 Subject: [PATCH 49/72] Add some custom validation rules --- scripts/config_mapping/integration_tests.py | 3 + scripts/config_mapping/lifecycle_config.py | 55 ++++-- .../basic_test/expected_output/hm_demo.json | 44 ++++- .../basic_test/expected_output/lm_demo.json | 8 +- .../tests/basic_test/input/lm_config.json | 10 +- .../input/lm_config.json | 8 +- .../expected_output/lm_demo.json | 2 +- .../empty_lm_config_test/input/lm_config.json | 4 +- .../expected_output/hm_demo.json | 44 ++++- .../expected_output/lm_demo.json | 158 ++++++++++++++++++ .../health_config_test/input/lm_config.json | 10 +- .../expected_output/hm_demo.json | 70 +++++++- ...-daemon.json => hmproc_someip-daemon.json} | 0 ...manager.json => hmproc_state_manager.json} | 0 .../{test_app1.json => hmproc_test_app1.json} | 0 .../{test_app2.json => hmproc_test_app2.json} | 0 .../expected_output/lm_demo.json | 10 +- .../tests/lm_config_test/input/lm_config.json | 12 +- 18 files changed, 368 insertions(+), 70 deletions(-) create mode 100644 scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json rename scripts/config_mapping/tests/lm_config_test/expected_output/{someip-daemon.json => hmproc_someip-daemon.json} (100%) rename scripts/config_mapping/tests/lm_config_test/expected_output/{state_manager.json => hmproc_state_manager.json} (100%) rename scripts/config_mapping/tests/lm_config_test/expected_output/{test_app1.json => hmproc_test_app1.json} (100%) rename scripts/config_mapping/tests/lm_config_test/expected_output/{test_app2.json => hmproc_test_app2.json} (100%) diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 1cd8f1c6..a0c201e4 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -6,6 +6,7 @@ import filecmp script_dir = Path(__file__).parent +schema_path = script_dir.parent.parent / "src" / "launch_manager_daemon" / "config" / "s-core_launch_manager.schema.json" tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" @@ -37,6 +38,8 @@ def run(input_file: Path, test_name: str, compare_files_only=[], exclude_files=[ str(input_file), "-o", str(actual_output_dir), + "--schema", + str(schema_path), ] try: diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 7412a23e..e638ca3f 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -22,7 +22,7 @@ }, "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "fallback_run_target" } }, "sandbox": { @@ -47,7 +47,7 @@ "transition_timeout": 5, "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "fallback_run_target" } } }, @@ -58,6 +58,9 @@ } """) +def report_error(message): + print(message, file=sys.stderr) + # There are various dictionaries in the config where only a single entry is allowed. # We do not want to merge the defaults with the user specified values for these dictionaries. not_merging_dicts = ["ready_recovery_action", "recovery_action"] @@ -71,11 +74,11 @@ def load_json_file(file_path: str) -> Dict[str, Any]: def get_recovery_process_group_state(config): fallback = config.get("fallback_run_target", None) - if fallback: - return "MainPG/fallback_run_target" - else: - return "MainPG/Off" - + if not fallback: + report_error( + "fallback_run_target not found, but it is a mandatory configuration.") + exit(1) + return "MainPG/fallback_run_target" def sec_to_ms(sec: float) -> int: return int(sec * 1000) @@ -195,6 +198,7 @@ def get_all_process_group_states(run_targets): process_group_states = [] for run_target, _ in run_targets.items(): process_group_states.append("MainPG/" + run_target) + process_group_states.append("MainPG/fallback_run_target") return process_group_states def get_all_refProcessGroupStates(run_targets): @@ -472,13 +476,6 @@ def get_terminating_behavior(component_config): else: return "ProcessIsNotSelfTerminating" - if "fallback_run_target" in config["run_targets"]: - print( - "Run target name fallback_run_target is reserved at the moment", - file=sys.stderr, - ) - exit(1) - lm_config = {} lm_config["versionMajor"] = 7 lm_config["versionMinor"] = 0 @@ -486,7 +483,7 @@ def get_terminating_behavior(component_config): lm_config["ModeGroup"] = [ { "identifier": "MainPG", - "initialMode_name": config.get("initial_run_target", "Off"), + "initialMode_name": "not-used", "recoveryMode_name": get_recovery_process_group_state(config), "modeDeclaration": [], } @@ -606,6 +603,31 @@ def get_terminating_behavior(component_config): with open(f"{output_dir}/lm_demo.json", "w") as lm_file: json.dump(lm_config, lm_file, indent=4) +def custom_validations(config): + success = True + + if config.get("initial_run_target") != "Startup": + report_error( + "initial_run_target must be configured to 'Startup'. Other values are not yet supported yet." + ) + success = False + + if "fallback_run_target" in config["run_targets"]: + report_error( + "Run target name \"fallback_run_target\" is reserved, please choose a different name." + ) + success = False + + # Check that for any switch_run_target recovery action, the run_target is set to "fallback_run_target" + for name, run_target in config["run_targets"].items(): + recovery_target_name = run_target.get("recovery_action", {}).get("switch_run_target", {}).get("run_target", "fallback_run_target") + if recovery_target_name != "fallback_run_target": + report_error("For any switch_run_target recovery action, the run_target must be set to \"fallback_run_target\".") + success = False + + return success + + def check_validation_dependency(): try: import jsonschema @@ -663,6 +685,9 @@ def main(): print("No schema provided, skipping validation. Provide the path to the json schema with \"--schema \" to enable validation.") preprocessed_config = preprocess_defaults(score_defaults, input_config) + if not custom_validations(preprocessed_config): + exit(1) + gen_health_monitor_config(args.output_dir, preprocessed_config) gen_launch_manager_config(args.output_dir, preprocessed_config) diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json index fd63bcc3..4186ddd9 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json @@ -9,16 +9,22 @@ "processType": "REGULAR_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ { "processExecutionError": 1 }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -31,16 +37,22 @@ "processType": "REGULAR_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ { "processExecutionError": 1 }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -53,16 +65,22 @@ "processType": "STM_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ { "processExecutionError": 1 }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -125,10 +143,13 @@ "refProcessIndex": 0, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] }, @@ -144,10 +165,13 @@ "refProcessIndex": 1, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] }, @@ -163,10 +187,13 @@ "refProcessIndex": 2, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] } @@ -230,10 +257,13 @@ ], "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] } diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json index 03bf7270..36303aac 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -33,7 +33,7 @@ "processGroupStateDependency": [ { "stateMachine_name": "MainPG", - "stateName": "MainPG/Minimal" + "stateName": "MainPG/Startup" }, { "stateMachine_name": "MainPG", @@ -301,7 +301,7 @@ "processGroupStateDependency": [ { "stateMachine_name": "MainPG", - "stateName": "MainPG/Minimal" + "stateName": "MainPG/Startup" }, { "stateMachine_name": "MainPG", @@ -346,11 +346,11 @@ "ModeGroup": [ { "identifier": "MainPG", - "initialMode_name": "Minimal", + "initialMode_name": "not-used", "recoveryMode_name": "MainPG/fallback_run_target", "modeDeclaration": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index 64807733..65b6326d 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -130,27 +130,27 @@ } }, "run_targets": { - "Minimal": { + "Startup": { "description": "Minimal functionality of the system", "depends_on": ["state_manager"], "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "fallback_run_target" } } }, "Full": { "description": "Everything running", - "depends_on": ["test_app1", "Minimal"], + "depends_on": ["test_app1", "Startup"], "transition_timeout": 5, "recovery_action": { "switch_run_target": { - "run_target": "Minimal" + "run_target": "fallback_run_target" } } } }, - "initial_run_target": "Minimal", + "initial_run_target": "Startup", "fallback_run_target": { "description": "Switching off everything", "depends_on": [], diff --git a/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json index 7b9cf70c..a19c2cd6 100644 --- a/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json @@ -41,12 +41,12 @@ } }, "run_targets": { - "Minimal": { + "Startup": { "description": "Minimal functionality of the system", "depends_on": ["non_supervised_comp"], "recovery_action": { "switch_run_target": { - "run_target": "Full" + "run_target": "fallback_run_target" } } }, @@ -56,12 +56,12 @@ "transition_timeout": 5, "recovery_action": { "switch_run_target": { - "run_target": "Minimal" + "run_target": "fallback_run_target" } } } }, - "initial_run_target": "Full", + "initial_run_target": "Startup", "fallback_run_target": { "description": "Switching off everything", "depends_on": [], diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json index 17f13a0f..75db51ce 100644 --- a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json @@ -5,7 +5,7 @@ "ModeGroup": [ { "identifier": "MainPG", - "initialMode_name": "Minimal", + "initialMode_name": "not-used", "recoveryMode_name": "MainPG/fallback_run_target", "modeDeclaration": [ { diff --git a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json index 5804c057..6a24824c 100644 --- a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json @@ -19,7 +19,7 @@ }, "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "fallback_run_target" } }, "sandbox": { @@ -63,7 +63,7 @@ "evaluation_cycle": 0.5 } }, - "initial_run_target": "Minimal", + "initial_run_target": "Startup", "fallback_run_target": { "description": "Switching off everything", "depends_on": [], diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json index db38da3d..589bf217 100644 --- a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json @@ -9,16 +9,22 @@ "processType": "STM_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ { "processExecutionError": 1 }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -31,16 +37,22 @@ "processType": "REGULAR_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ { "processExecutionError": 1 }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -53,16 +65,22 @@ "processType": "REGULAR_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ { "processExecutionError": 1 }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -125,10 +143,13 @@ "refProcessIndex": 0, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] }, @@ -144,10 +165,13 @@ "refProcessIndex": 1, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] }, @@ -163,10 +187,13 @@ "refProcessIndex": 2, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] } @@ -230,10 +257,13 @@ ], "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] } diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json new file mode 100644 index 00000000..8ac49246 --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json @@ -0,0 +1,158 @@ +{ + "versionMajor": 7, + "versionMinor": 0, + "Process": [ + { + "identifier": "non_supervised_comp", + "path": "/opt/scripts/bin/comp", + "uid": 3, + "gid": 0, + "sgids": [], + "securityPolicyDetails": "", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "non_supervised_comp_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsSelfTerminating", + "processGroupStateDependency": [], + "environmentVariable": [], + "processArgument": [], + "executionDependency": [] + } + ] + }, + { + "identifier": "state_manager", + "path": "/opt/apps/state_manager/sm", + "uid": 4, + "gid": 0, + "sgids": [], + "securityPolicyDetails": "", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "functionClusterAffiliation": "STATE_MANAGEMENT", + "startupConfig": [ + { + "executionError": "1", + "identifier": "state_manager_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Startup" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [], + "processArgument": [], + "executionDependency": [] + } + ] + }, + { + "identifier": "reporting_supervised_component", + "path": "/opt/scripts/bin/comp", + "uid": 5, + "gid": 0, + "sgids": [], + "securityPolicyDetails": "", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "reporting_supervised_component_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "reporting_supervised_component_with_no_max_indications", + "path": "/opt/scripts/bin/comp", + "uid": 5, + "gid": 0, + "sgids": [], + "securityPolicyDetails": "", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "reporting_supervised_component_with_no_max_indications_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + } + ], + "executionDependency": [] + } + ] + } + ], + "ModeGroup": [ + { + "identifier": "MainPG", + "initialMode_name": "not-used", + "recoveryMode_name": "MainPG/fallback_run_target", + "modeDeclaration": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/input/lm_config.json b/scripts/config_mapping/tests/health_config_test/input/lm_config.json index 76c239aa..679e1083 100644 --- a/scripts/config_mapping/tests/health_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/health_config_test/input/lm_config.json @@ -99,27 +99,27 @@ } }, "run_targets": { - "Minimal": { + "Startup": { "description": "Minimal functionality of the system", "depends_on": ["state_manager"], "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "fallback_run_target" } } }, "Full": { "description": "Everything running", - "depends_on": ["reporting_supervised_component", "reporting_supervised_component_with_no_max_indications", "Minimal"], + "depends_on": ["reporting_supervised_component", "reporting_supervised_component_with_no_max_indications", "Startup"], "transition_timeout": 5, "recovery_action": { "switch_run_target": { - "run_target": "Minimal" + "run_target": "fallback_run_target" } } } }, - "initial_run_target": "Full", + "initial_run_target": "Startup", "fallback_run_target": { "description": "Switching off everything", "depends_on": [], diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json index b423bc8d..619e3082 100644 --- a/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json @@ -9,13 +9,22 @@ "processType": "REGULAR_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -28,13 +37,22 @@ "processType": "REGULAR_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -47,13 +65,22 @@ "processType": "REGULAR_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -66,13 +93,22 @@ "processType": "STM_PROCESS", "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ], "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, { "processExecutionError": 1 } @@ -148,10 +184,13 @@ "refProcessIndex": 0, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] }, @@ -167,10 +206,13 @@ "refProcessIndex": 1, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] }, @@ -186,10 +228,13 @@ "refProcessIndex": 2, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] }, @@ -205,10 +250,13 @@ "refProcessIndex": 3, "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] } @@ -287,16 +335,20 @@ ], "refProcessGroupStates": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" } ] } ], "hmRecoveryNotification": [ { + "shortName": "recovery_notification", "recoveryNotificationTimeout": 5000, "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", "refGlobalSupervisionIndex": 0, diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/someip-daemon.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_someip-daemon.json similarity index 100% rename from scripts/config_mapping/tests/lm_config_test/expected_output/someip-daemon.json rename to scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_someip-daemon.json diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/state_manager.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_state_manager.json similarity index 100% rename from scripts/config_mapping/tests/lm_config_test/expected_output/state_manager.json rename to scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_state_manager.json diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/test_app1.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app1.json similarity index 100% rename from scripts/config_mapping/tests/lm_config_test/expected_output/test_app1.json rename to scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app1.json diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/test_app2.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app2.json similarity index 100% rename from scripts/config_mapping/tests/lm_config_test/expected_output/test_app2.json rename to scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app2.json diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json index ec608cef..ee75721c 100644 --- a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json @@ -30,7 +30,7 @@ "processGroupStateDependency": [ { "stateMachine_name": "MainPG", - "stateName": "MainPG/Minimal" + "stateName": "MainPG/Startup" }, { "stateMachine_name": "MainPG", @@ -101,7 +101,7 @@ "processGroupStateDependency": [ { "stateMachine_name": "MainPG", - "stateName": "MainPG/Minimal" + "stateName": "MainPG/Startup" }, { "stateMachine_name": "MainPG", @@ -374,7 +374,7 @@ "processGroupStateDependency": [ { "stateMachine_name": "MainPG", - "stateName": "MainPG/Minimal" + "stateName": "MainPG/Startup" }, { "stateMachine_name": "MainPG", @@ -419,11 +419,11 @@ "ModeGroup": [ { "identifier": "MainPG", - "initialMode_name": "Minimal", + "initialMode_name": "not-used", "recoveryMode_name": "MainPG/fallback_run_target", "modeDeclaration": [ { - "identifier": "MainPG/Minimal" + "identifier": "MainPG/Startup" }, { "identifier": "MainPG/Full" diff --git a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json index 90fe43d5..e244503a 100644 --- a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json @@ -19,7 +19,7 @@ }, "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "fallback_run_target" } }, "sandbox": { @@ -153,27 +153,27 @@ } }, "run_targets": { - "Minimal": { + "Startup": { "description": "Minimal functionality of the system", "depends_on": ["state_manager"], "recovery_action": { "switch_run_target": { - "run_target": "Off" + "run_target": "fallback_run_target" } } }, "Full": { "description": "Everything running", - "depends_on": ["test_app1", "Minimal"], + "depends_on": ["test_app1", "Startup"], "transition_timeout": 5, "recovery_action": { "switch_run_target": { - "run_target": "Minimal" + "run_target": "fallback_run_target" } } } }, - "initial_run_target": "Minimal", + "initial_run_target": "Startup", "fallback_run_target": { "description": "Switching off everything", "depends_on": [], From ae6da75cdecb98d9888246c84d03690bf1b77e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 27 Feb 2026 14:58:31 +0100 Subject: [PATCH 50/72] Migrate smoketest to new configuration --- scripts/config_mapping/config.bzl | 11 +- scripts/config_mapping/requirements.txt | 2 + .../common/include/score/lcm/internal/log.hpp | 2 +- .../src/score/lcm/saf/logging/PhmLogger.hpp | 2 +- .../configurationmanager.cpp | 6 +- .../src/process_group_manager/graph.cpp | 2 +- tests/integration/smoke/BUILD | 23 ++-- .../integration/smoke/control_daemon_mock.cpp | 26 ++-- tests/integration/smoke/gtest_process.cpp | 1 - tests/integration/smoke/hm_demo.json | 13 -- .../smoke/lifecycle_smoketest.json | 116 +++++++++++++++++ tests/integration/smoke/lm_demo.json | 123 ------------------ tests/integration/smoke/smoke.py | 3 + 13 files changed, 154 insertions(+), 176 deletions(-) create mode 100644 scripts/config_mapping/requirements.txt delete mode 100644 tests/integration/smoke/hm_demo.json create mode 100644 tests/integration/smoke/lifecycle_smoketest.json delete mode 100644 tests/integration/smoke/lm_demo.json diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl index 12e5ce9b..24826476 100644 --- a/scripts/config_mapping/config.bzl +++ b/scripts/config_mapping/config.bzl @@ -66,8 +66,15 @@ def _gen_lifecycle_config_impl(ctx): ), arguments = [] ) - - return DefaultInfo(files = depset([gen_dir_flatbuffer])) + + rf = ctx.runfiles( + files = [gen_dir_flatbuffer], + root_symlinks = { + ("_main/" + ctx.attr.flatbuffer_out_dir): gen_dir_flatbuffer, + } + ) + + return DefaultInfo(files = depset([gen_dir_flatbuffer]), runfiles = rf) gen_lifecycle_config = rule( diff --git a/scripts/config_mapping/requirements.txt b/scripts/config_mapping/requirements.txt new file mode 100644 index 00000000..8d45edf7 --- /dev/null +++ b/scripts/config_mapping/requirements.txt @@ -0,0 +1,2 @@ +pytest +jsonschema diff --git a/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp b/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp index de02e145..5d0c4a0b 100644 --- a/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp +++ b/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp @@ -88,7 +88,7 @@ inline LogLevel GetLevelFromEnv() { } } else { - return LogLevel::kInfo; + return LogLevel::kDebug; } } diff --git a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp index aa9154e1..6acb3fdb 100644 --- a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp +++ b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp @@ -72,7 +72,7 @@ inline LogLevel GetLevelFromEnv() { } } else { - return LogLevel::kInfo; + return LogLevel::kDebug; } } diff --git a/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp b/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp index 0d79443d..cb0420e2 100644 --- a/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp +++ b/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp @@ -304,14 +304,14 @@ bool ConfigurationManager::parseMachineConfigurations(const ModeGroup* node, con process_group_data.name_ = getStringViewFromFlatBuffer(node->identifier()); process_group_data.sw_cluster_ = cluster; LM_LOG_DEBUG() << "FlatBufferParser::getModeGroupPgName:" << getStringFromFlatBuffer(node->identifier()) - << "( IdentifierHash:" << process_group_data.name_ << ")"; + << "( IdentifierHash:" << process_group_data.name_.data() << ")"; if (process_group_data.name_ != score::lcm::IdentifierHash(std::string_view(""))) { // Add process group name to the PG name list process_group_names_.push_back(process_group_data.name_); result = parseModeGroups(node, process_group_data); } else { - LM_LOG_WARN() << "parseMachineConfigurations: Process group name is empty furz"; + LM_LOG_WARN() << "parseMachineConfigurations: Process group name is empty"; } } return result; @@ -339,7 +339,7 @@ bool ConfigurationManager::parseModeGroups(const ModeGroup* node, ProcessGroup& pg_state.name_ = getStringViewFromFlatBuffer(flatbuffer_string); LM_LOG_DEBUG() << "FlatBufferParser::getModeGroupPgStateName:" << mode_declaration_node->identifier()->c_str() - << "( IdentifierHash:" << pg_state.name_ << ")"; + << "( IdentifierHash:" << pg_state.name_.data() << ")"; process_group_data.states_.push_back(pg_state); // Is this the "Off" state, i.e. does it end with "/Off" ? auto str_len = string_name.length(); diff --git a/src/launch_manager_daemon/src/process_group_manager/graph.cpp b/src/launch_manager_daemon/src/process_group_manager/graph.cpp index 5866c0b5..a5b976fa 100644 --- a/src/launch_manager_daemon/src/process_group_manager/graph.cpp +++ b/src/launch_manager_daemon/src/process_group_manager/graph.cpp @@ -39,7 +39,7 @@ Graph::Graph(uint32_t max_num_nodes, ProcessGroupManager* pgm) last_state_manager_(), last_execution_error_(0U), is_initial_state_transition_(false), - pending_state_(""), + pending_state_("(Initial)"), event_(ControlClientCode::kNotSet), cancel_message_(), request_start_time_() { diff --git a/tests/integration/smoke/BUILD b/tests/integration/smoke/BUILD index 86ea1591..edda97da 100644 --- a/tests/integration/smoke/BUILD +++ b/tests/integration/smoke/BUILD @@ -14,25 +14,21 @@ load("@score_lifecycle_pip//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") load("//config:flatbuffers_rules.bzl", "flatbuffer_json_to_bin") -flatbuffer_json_to_bin( - name = "test_lm_cfg", - json = "lm_demo.json", - schema = "//src/launch_manager_daemon:config/lm_flatcfg.fbs", -) +exports_files( + ["lm_demo.json"], + visibility = ["//examples:__subpackages__"]) -flatbuffer_json_to_bin( - name = "test_hm_cfg", - json = "hm_demo.json", - schema = "//src/launch_manager_daemon/health_monitor_lib:config/hm_flatcfg.fbs", +load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") +gen_lifecycle_config( + name ="lm_smoketest_config", + config="//tests/integration/smoke:lifecycle_smoketest.json", + flatbuffer_out_dir="etc" ) cc_binary( name = "control_daemon_mock", srcs = ["control_daemon_mock.cpp"], - data = [ - ":test_hm_cfg", - ":test_lm_cfg", - ], + data = [], deps = [ "//src/control_client_lib", "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", @@ -63,6 +59,7 @@ score_py_pytest( ":control_daemon_mock", ":gtest_process", "//src/launch_manager_daemon:launch_manager", + ":lm_smoketest_config" ], tags = [ "integration", diff --git a/tests/integration/smoke/control_daemon_mock.cpp b/tests/integration/smoke/control_daemon_mock.cpp index aeea0ae4..90139c75 100644 --- a/tests/integration/smoke/control_daemon_mock.cpp +++ b/tests/integration/smoke/control_daemon_mock.cpp @@ -20,37 +20,27 @@ #include #include "tests/integration/test_helper.hpp" -score::lcm::ControlClient client([](const score::lcm::ExecutionErrorEvent& event) { - std::cerr << "Undefined state callback invoked for process group id: " << event.processGroup.data() << std::endl; -}); - -// create DefaultPG -const score::lcm::IdentifierHash defaultpg {"DefaultPG"}; -const score::lcm::IdentifierHash defaultpgOn {"DefaultPG/On"}; -const score::lcm::IdentifierHash defaultpgOff {"DefaultPG/Off"}; -// MainPG -const score::lcm::IdentifierHash mainpg {"MainPG"}; -const score::lcm::IdentifierHash mainpgOff {"MainPG/Off"}; +score::lcm::ControlClient client; TEST(Smoke, Daemon) { TEST_STEP("Control daemon report kRunning") { // report kRunning auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); - ASSERT_TRUE(result.has_value()) << "client.ReportExecutionState() failed"; } - TEST_STEP("Turn default PG on") { + TEST_STEP("Activate RunTarget Running") { score::cpp::stop_token stop_token; - auto result = client.SetState(defaultpg, defaultpgOn).Get(stop_token); + auto result2 = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + auto result = client.ActivateRunTarget("Running").Get(stop_token); EXPECT_TRUE(result.has_value()); } - TEST_STEP("Turn default PG off") { + TEST_STEP("Activate RunTarget Startup") { score::cpp::stop_token stop_token; - auto result = client.SetState(defaultpg, defaultpgOff).Get(stop_token); + auto result = client.ActivateRunTarget("Startup").Get(stop_token); EXPECT_TRUE(result.has_value()); } - TEST_STEP("Turn main PG off") { - client.SetState(mainpg, mainpgOff); + TEST_STEP("Activate RunTarget Off") { + client.ActivateRunTarget("Off"); } } diff --git a/tests/integration/smoke/gtest_process.cpp b/tests/integration/smoke/gtest_process.cpp index 702e51b9..5c13582f 100644 --- a/tests/integration/smoke/gtest_process.cpp +++ b/tests/integration/smoke/gtest_process.cpp @@ -12,7 +12,6 @@ ********************************************************************************/ #include -#include #include #include diff --git a/tests/integration/smoke/hm_demo.json b/tests/integration/smoke/hm_demo.json deleted file mode 100644 index 8e559853..00000000 --- a/tests/integration/smoke/hm_demo.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "versionMajor": 8, - "versionMinor": 0, - "process": [], - "hmMonitorInterface": [], - "hmSupervisionCheckpoint": [], - "hmAliveSupervision": [], - "hmDeadlineSupervision": [], - "hmLogicalSupervision": [], - "hmLocalSupervision": [], - "hmGlobalSupervision": [], - "hmRecoveryNotification": [] -} \ No newline at end of file diff --git a/tests/integration/smoke/lifecycle_smoketest.json b/tests/integration/smoke/lifecycle_smoketest.json new file mode 100644 index 00000000..f0ba213e --- /dev/null +++ b/tests/integration/smoke/lifecycle_smoketest.json @@ -0,0 +1,116 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tests/integration/smoke", + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Startup" + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_daemon": { + "component_properties": { + "binary_name": "control_daemon_mock", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_daemon" + } + } + }, + "gtest_process": { + "component_properties": { + "binary_name": "gtest_process", + "application_profile": { + "application_type": "Reporting" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "DefaultPG_app0" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_daemon" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "Startup" + } + } + }, + "Running": { + "depends_on": [ + "control_daemon", + "gtest_process" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "Startup" + } + } + }, + "Off": { + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + + } + } + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} \ No newline at end of file diff --git a/tests/integration/smoke/lm_demo.json b/tests/integration/smoke/lm_demo.json deleted file mode 100644 index 10185fc8..00000000 --- a/tests/integration/smoke/lm_demo.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "versionMajor": 7, - "versionMinor": 0, - "Process": [ - { - "identifier": "control_daemon", - "uid": 0, - "gid": 0, - "path": "/tests/integration/smoke/control_daemon_mock", - "functionClusterAffiliation": "STATE_MANAGEMENT", - "numberOfRestartAttempts": 0, - "executable_reportingBehavior": "ReportsExecutionState", - "sgids": [], - "startupConfig": [ - { - "executionError": "1", - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "identifier": "control_daemon_startup_config", - "enterTimeoutValue": 1000, - "exitTimeoutValue": 1000, - "terminationBehavior": "ProcessIsNotSelfTerminating", - "executionDependency": [], - "processGroupStateDependency": [ - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Startup" - }, - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Recovery" - } - ], - "environmentVariable": [ - { - "key": "LD_LIBRARY_PATH", - "value": "/opt/lib" - }, - { - "key": "PROCESSIDENTIFIER", - "value": "control_daemon" - } - ], - "processArgument": [] - } - ] - }, - { - "identifier": "demo_app0_DefaultPG", - "uid": 0, - "gid": 0, - "path": "/tests/integration/smoke/gtest_process", - "numberOfRestartAttempts": 0, - "executable_reportingBehavior": "ReportsExecutionState", - "sgids": [], - "startupConfig": [ - { - "executionError": "1", - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "identifier": "demo_app_startup_config_0", - "enterTimeoutValue": 2000, - "exitTimeoutValue": 2000, - "terminationBehavior": "ProcessIsNotSelfTerminating", - "processGroupStateDependency": [ - { - "stateMachine_name": "DefaultPG", - "stateName": "DefaultPG/On" - } - ], - "environmentVariable": [ - { - "key": "LD_LIBRARY_PATH", - "value": "/opt/lib" - }, - { - "key": "PROCESSIDENTIFIER", - "value": "DefaultPG_app0" - }, - { - "key": "CONFIG_PATH", - "value": "/opt/supervision_demo/etc/health_monitor_process_cfg_0_MainPG.bin" - } - ] - } - ] - } - ], - "ModeGroup": [ - { - "identifier": "MainPG", - "initialMode_name": "Off", - "recoveryMode_name": "MainPG/Recovery", - "modeDeclaration": [ - { - "identifier": "MainPG/Off" - }, - { - "identifier": "MainPG/Startup" - }, - { - "identifier": "MainPG/Recovery" - } - ] - }, - { - "identifier": "DefaultPG", - "initialMode_name": "Off", - "recoveryMode_name": "DefaultPG/Recovery", - "modeDeclaration": [ - { - "identifier": "DefaultPG/Off" - }, - { - "identifier": "DefaultPG/On" - }, - { - "identifier": "DefaultPG/Recovery" - } - ] - } - ] -} \ No newline at end of file diff --git a/tests/integration/smoke/smoke.py b/tests/integration/smoke/smoke.py index 75fd6e5b..2d6dcd1a 100644 --- a/tests/integration/smoke/smoke.py +++ b/tests/integration/smoke/smoke.py @@ -25,5 +25,8 @@ def test_smoke(): print(format_logs(code, stdout, stderr)) + with open("exm_logs.txt", "w") as f: + f.write(format_logs(code, stdout, stderr)) + check_for_failures(Path("tests/integration/smoke"), 2) assert code == 0 From edcf3a7c2381cad3a878c248b14b1e06ac7fa688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 3 Mar 2026 11:37:28 +0100 Subject: [PATCH 51/72] More integration tests --- scripts/config_mapping/integration_tests.py | 55 ++++++ scripts/config_mapping/lifecycle_config.py | 48 ++++-- .../input/lm_config.json | 46 +++++ .../expected_output/lm_demo.json | 3 + .../empty_lm_config_test/input/lm_config.json | 11 ++ .../expected_output/lm_demo.json | 158 ------------------ .../input/lm_config.json | 22 +++ 7 files changed, 169 insertions(+), 174 deletions(-) create mode 100644 scripts/config_mapping/tests/custom_validation_failures_test/input/lm_config.json delete mode 100644 scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json create mode 100644 scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index a0c201e4..a4ca1742 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -4,6 +4,12 @@ import shutil from pathlib import Path import filecmp +from lifecycle_config import ( + SUCCESS, + SCHEMA_VALIDATION_DEPENDENCY_ERROR, + SCHEMA_VALIDATION_FAILURE, + CUSTOM_VALIDATION_FAILURE, +) script_dir = Path(__file__).parent schema_path = script_dir.parent.parent / "src" / "launch_manager_daemon" / "config" / "s-core_launch_manager.schema.json" @@ -154,3 +160,52 @@ def test_empty_launch_config_mapping(): input_file = tests_dir / test_name / "input" / "lm_config.json" run(input_file, test_name, compare_files_only=["lm_demo.json"]) + +def test_custom_validation_failures(): + """ + Test that custom validation checks implemented in lifecycle_config.py are correctly identifying invalid configurations. + The input configuration contains the following issues: + * The run target "Minimal" has a recovery action that switches to a run target "Full" instead of "fallback_run_target" + * The mandatory "fallback_run_target" is missing from the configuration + * Reserved name "fallback_run_target" is used for a RunTarget name which is not allowed + * Initial RunTarget is not configured to "Startup" + * The "Startup" RunTarget is missing from the configuration, which is mandatory + """ + test_name = "custom_validation_failures_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + try: + run(input_file, test_name) + raise AssertionError("Expected an error due to custom validation failures, but the mapping script executed successfully.") + except subprocess.CalledProcessError as e: + assert e.returncode == CUSTOM_VALIDATION_FAILURE, f"Expected exit code {CUSTOM_VALIDATION_FAILURE}, got {e.returncode}" + + expected_errors = [ + "recovery RunTarget must be set to \"fallback_run_target\"", + "fallback_run_target is a mandatory configuration", + "RunTarget name \"fallback_run_target\" is reserved", + "initial_run_target must be configured to 'Startup'", + "\"Startup\" is a mandatory RunTarget" + ] + actual_error_output = e.stderr + for expected_error in expected_errors: + if expected_error not in actual_error_output: + print(f"Expected error message not found: {expected_error}") + print(f"Actual error output: {actual_error_output}") + raise AssertionError(f"Expected error message not found: {expected_error}") + + +def test_schema_validation_failures(): + """ + Test that schema validation errors are correctly raised when the input configuration does not conform to the defined JSON schema. + The input configuration contains the following issues: + * Missing required fields + """ + test_name = "schema_validation_failure_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + try: + run(input_file, test_name) + raise AssertionError("Expected an error due to schema validation failures, but the mapping script executed successfully.") + except subprocess.CalledProcessError as e: + assert e.returncode == SCHEMA_VALIDATION_FAILURE, f"Expected exit code {SCHEMA_VALIDATION_FAILURE}, got {e.returncode}" diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index e638ca3f..c67853dd 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -73,11 +73,7 @@ def load_json_file(file_path: str) -> Dict[str, Any]: def get_recovery_process_group_state(config): - fallback = config.get("fallback_run_target", None) - if not fallback: - report_error( - "fallback_run_target not found, but it is a mandatory configuration.") - exit(1) + # Existence has already been validated in the custom_validations function return "MainPG/fallback_run_target" def sec_to_ms(sec: float) -> int: @@ -612,19 +608,30 @@ def custom_validations(config): ) success = False + if "Startup" not in config["run_targets"]: + report_error( + "\"Startup\" is a mandatory RunTarget and must be defined in the configuration." + ) + success = False + + if "fallback_run_target" in config["run_targets"]: report_error( - "Run target name \"fallback_run_target\" is reserved, please choose a different name." + "RunTarget name \"fallback_run_target\" is reserved, please choose a different name." ) success = False # Check that for any switch_run_target recovery action, the run_target is set to "fallback_run_target" - for name, run_target in config["run_targets"].items(): + for _, run_target in config["run_targets"].items(): recovery_target_name = run_target.get("recovery_action", {}).get("switch_run_target", {}).get("run_target", "fallback_run_target") if recovery_target_name != "fallback_run_target": - report_error("For any switch_run_target recovery action, the run_target must be set to \"fallback_run_target\".") + report_error("For any switch_run_target recovery action, the recovery RunTarget must be set to \"fallback_run_target\".") success = False + if "fallback_run_target" not in config: + report_error("fallback_run_target is a mandatory configuration but was not found in the config.") + success = False + return success @@ -643,9 +650,14 @@ def schema_validation(json_input, schema): print("Schema Validation successful") return True except ValidationError as err: - print(err) + print(err, file=sys.stderr) return False +# Possible exit codes returned from this script +SUCCESS = 0 +SCHEMA_VALIDATION_DEPENDENCY_ERROR = 1 +SCHEMA_VALIDATION_FAILURE = 2 +CUSTOM_VALIDATION_FAILURE = 3 def main(): parser = argparse.ArgumentParser() @@ -667,7 +679,7 @@ def main(): if args.schema: # User asked for validation explicitly, but the dependency is not installed, we should exit with an error if args.validate and not check_validation_dependency(): - exit(1) + exit(SCHEMA_VALIDATION_DEPENDENCY_ERROR) # User asked not explicitly for validation, but the dependency is not installed, we should print a warning and continue without validation if not check_validation_dependency(): @@ -677,21 +689,25 @@ def main(): json_schema = load_json_file(args.schema) validation_successful = schema_validation(input_config, json_schema) if not validation_successful: - exit(1) + exit(SCHEMA_VALIDATION_FAILURE) if args.validate: - exit(0) + exit(SUCCESS) else: print("No schema provided, skipping validation. Provide the path to the json schema with \"--schema \" to enable validation.") preprocessed_config = preprocess_defaults(score_defaults, input_config) if not custom_validations(preprocessed_config): - exit(1) + exit(CUSTOM_VALIDATION_FAILURE) - gen_health_monitor_config(args.output_dir, preprocessed_config) - gen_launch_manager_config(args.output_dir, preprocessed_config) + try: + gen_health_monitor_config(args.output_dir, preprocessed_config) + gen_launch_manager_config(args.output_dir, preprocessed_config) + except ValueError as e: + print(f"Error during configuration generation: {e}", file=sys.stderr) + exit(CUSTOM_VALIDATION_FAILURE) - return 0 + return SUCCESS if __name__ == "__main__": diff --git a/scripts/config_mapping/tests/custom_validation_failures_test/input/lm_config.json b/scripts/config_mapping/tests/custom_validation_failures_test/input/lm_config.json new file mode 100644 index 00000000..66a51152 --- /dev/null +++ b/scripts/config_mapping/tests/custom_validation_failures_test/input/lm_config.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "defaults": {}, + "components": { + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1" + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["test_app1"], + "recovery_action": { + "switch_run_target": { + "run_target": "Fallback" + } + } + }, + "Fallback": { + "description": "Nothing running", + "depends_on": [], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "fallback_run_target": { + "description": "Fallback run target", + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Minimal" +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json index 75db51ce..e51df1ee 100644 --- a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json @@ -8,6 +8,9 @@ "initialMode_name": "not-used", "recoveryMode_name": "MainPG/fallback_run_target", "modeDeclaration": [ + { + "identifier": "MainPG/Startup" + }, { "identifier": "MainPG/fallback_run_target" } diff --git a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json index 6a24824c..66be2556 100644 --- a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json @@ -63,6 +63,17 @@ "evaluation_cycle": 0.5 } }, + "run_targets": { + "Startup": { + "description": "Empty", + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, "initial_run_target": "Startup", "fallback_run_target": { "description": "Switching off everything", diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json deleted file mode 100644 index 8ac49246..00000000 --- a/scripts/config_mapping/tests/health_config_test/expected_output/lm_demo.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "versionMajor": 7, - "versionMinor": 0, - "Process": [ - { - "identifier": "non_supervised_comp", - "path": "/opt/scripts/bin/comp", - "uid": 3, - "gid": 0, - "sgids": [], - "securityPolicyDetails": "", - "numberOfRestartAttempts": 1, - "executable_reportingBehavior": "DoesNotReportExecutionState", - "startupConfig": [ - { - "executionError": "1", - "identifier": "non_supervised_comp_startup_config", - "enterTimeoutValue": 500, - "exitTimeoutValue": 500, - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "terminationBehavior": "ProcessIsSelfTerminating", - "processGroupStateDependency": [], - "environmentVariable": [], - "processArgument": [], - "executionDependency": [] - } - ] - }, - { - "identifier": "state_manager", - "path": "/opt/apps/state_manager/sm", - "uid": 4, - "gid": 0, - "sgids": [], - "securityPolicyDetails": "", - "numberOfRestartAttempts": 1, - "executable_reportingBehavior": "ReportsExecutionState", - "functionClusterAffiliation": "STATE_MANAGEMENT", - "startupConfig": [ - { - "executionError": "1", - "identifier": "state_manager_startup_config", - "enterTimeoutValue": 500, - "exitTimeoutValue": 500, - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "terminationBehavior": "ProcessIsNotSelfTerminating", - "processGroupStateDependency": [ - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Startup" - }, - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Full" - } - ], - "environmentVariable": [], - "processArgument": [], - "executionDependency": [] - } - ] - }, - { - "identifier": "reporting_supervised_component", - "path": "/opt/scripts/bin/comp", - "uid": 5, - "gid": 0, - "sgids": [], - "securityPolicyDetails": "", - "numberOfRestartAttempts": 1, - "executable_reportingBehavior": "ReportsExecutionState", - "startupConfig": [ - { - "executionError": "1", - "identifier": "reporting_supervised_component_startup_config", - "enterTimeoutValue": 500, - "exitTimeoutValue": 500, - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "terminationBehavior": "ProcessIsSelfTerminating", - "processGroupStateDependency": [ - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Full" - } - ], - "environmentVariable": [], - "processArgument": [ - { - "argument": "-a" - }, - { - "argument": "-b" - } - ], - "executionDependency": [] - } - ] - }, - { - "identifier": "reporting_supervised_component_with_no_max_indications", - "path": "/opt/scripts/bin/comp", - "uid": 5, - "gid": 0, - "sgids": [], - "securityPolicyDetails": "", - "numberOfRestartAttempts": 1, - "executable_reportingBehavior": "ReportsExecutionState", - "startupConfig": [ - { - "executionError": "1", - "identifier": "reporting_supervised_component_with_no_max_indications_startup_config", - "enterTimeoutValue": 500, - "exitTimeoutValue": 500, - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "terminationBehavior": "ProcessIsSelfTerminating", - "processGroupStateDependency": [ - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Full" - } - ], - "environmentVariable": [], - "processArgument": [ - { - "argument": "-a" - }, - { - "argument": "-b" - } - ], - "executionDependency": [] - } - ] - } - ], - "ModeGroup": [ - { - "identifier": "MainPG", - "initialMode_name": "not-used", - "recoveryMode_name": "MainPG/fallback_run_target", - "modeDeclaration": [ - { - "identifier": "MainPG/Startup" - }, - { - "identifier": "MainPG/Full" - }, - { - "identifier": "MainPG/fallback_run_target" - } - ] - } - ] -} \ No newline at end of file diff --git a/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json b/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json new file mode 100644 index 00000000..149c5161 --- /dev/null +++ b/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "defaults": {}, + "components": { + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1" + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + } + }, + "run_targets": { + "Minimal": { + "description": "Run Target with missing recovery action", + "depends_on": [] + } + }, + "initial_run_target": "Minimal" +} \ No newline at end of file From f2e6e8645cd3645cbd5b9e3a58f599ac24adb05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 5 Mar 2026 06:22:37 +0100 Subject: [PATCH 52/72] Revert debugging changes --- .../common/include/score/lcm/internal/log.hpp | 2 +- .../health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp | 2 +- tests/integration/smoke/control_daemon_mock.cpp | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp b/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp index 5d0c4a0b..de02e145 100644 --- a/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp +++ b/src/launch_manager_daemon/common/include/score/lcm/internal/log.hpp @@ -88,7 +88,7 @@ inline LogLevel GetLevelFromEnv() { } } else { - return LogLevel::kDebug; + return LogLevel::kInfo; } } diff --git a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp index 6acb3fdb..aa9154e1 100644 --- a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp +++ b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/logging/PhmLogger.hpp @@ -72,7 +72,7 @@ inline LogLevel GetLevelFromEnv() { } } else { - return LogLevel::kDebug; + return LogLevel::kInfo; } } diff --git a/tests/integration/smoke/control_daemon_mock.cpp b/tests/integration/smoke/control_daemon_mock.cpp index 90139c75..95f46126 100644 --- a/tests/integration/smoke/control_daemon_mock.cpp +++ b/tests/integration/smoke/control_daemon_mock.cpp @@ -30,7 +30,6 @@ TEST(Smoke, Daemon) { } TEST_STEP("Activate RunTarget Running") { score::cpp::stop_token stop_token; - auto result2 = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); auto result = client.ActivateRunTarget("Running").Get(stop_token); EXPECT_TRUE(result.has_value()); } From 2854564c064c0f562fac061748c26c4a5a5eb833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 3 Mar 2026 11:53:52 +0100 Subject: [PATCH 53/72] Fix example scenario configuration --- examples/config/lifecycle_demo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/config/lifecycle_demo.json b/examples/config/lifecycle_demo.json index 4d12f412..27583cd8 100644 --- a/examples/config/lifecycle_demo.json +++ b/examples/config/lifecycle_demo.json @@ -127,7 +127,7 @@ ], "recovery_action": { "switch_run_target": { - "run_target": "Startup" + "run_target": "fallback_run_target" } } }, @@ -140,7 +140,7 @@ ], "recovery_action": { "switch_run_target": { - "run_target": "Startup" + "run_target": "fallback_run_target" } } } From 761d3977e34b3441391b1faa9f0b8cfebe825a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 5 Mar 2026 06:24:52 +0100 Subject: [PATCH 54/72] Correct smoketest configuration --- tests/integration/smoke/lifecycle_smoketest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/smoke/lifecycle_smoketest.json b/tests/integration/smoke/lifecycle_smoketest.json index f0ba213e..1da59b6e 100644 --- a/tests/integration/smoke/lifecycle_smoketest.json +++ b/tests/integration/smoke/lifecycle_smoketest.json @@ -12,7 +12,7 @@ }, "recovery_action": { "switch_run_target": { - "run_target": "Startup" + "run_target": "fallback_run_target" } }, "environmental_variables": { @@ -81,7 +81,7 @@ ], "recovery_action": { "switch_run_target": { - "run_target": "Startup" + "run_target": "fallback_run_target" } } }, @@ -92,7 +92,7 @@ ], "recovery_action": { "switch_run_target": { - "run_target": "Startup" + "run_target": "fallback_run_target" } } }, From d88b0ecf19e2b23411500050a1acec1802e03295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 5 Mar 2026 09:00:04 +0100 Subject: [PATCH 55/72] Revert more debugging changes --- src/launch_manager_daemon/src/process_group_manager/graph.cpp | 2 +- tests/integration/smoke/smoke.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/launch_manager_daemon/src/process_group_manager/graph.cpp b/src/launch_manager_daemon/src/process_group_manager/graph.cpp index a5b976fa..5866c0b5 100644 --- a/src/launch_manager_daemon/src/process_group_manager/graph.cpp +++ b/src/launch_manager_daemon/src/process_group_manager/graph.cpp @@ -39,7 +39,7 @@ Graph::Graph(uint32_t max_num_nodes, ProcessGroupManager* pgm) last_state_manager_(), last_execution_error_(0U), is_initial_state_transition_(false), - pending_state_("(Initial)"), + pending_state_(""), event_(ControlClientCode::kNotSet), cancel_message_(), request_start_time_() { diff --git a/tests/integration/smoke/smoke.py b/tests/integration/smoke/smoke.py index 2d6dcd1a..75fd6e5b 100644 --- a/tests/integration/smoke/smoke.py +++ b/tests/integration/smoke/smoke.py @@ -25,8 +25,5 @@ def test_smoke(): print(format_logs(code, stdout, stderr)) - with open("exm_logs.txt", "w") as f: - f.write(format_logs(code, stdout, stderr)) - check_for_failures(Path("tests/integration/smoke"), 2) assert code == 0 From a78c449df2f45f8ae9916a61322c4f924f077d1a Mon Sep 17 00:00:00 2001 From: "Empting Eelco (ETAS-ECM/XPC-Fe2)" Date: Wed, 4 Mar 2026 15:31:00 +0100 Subject: [PATCH 56/72] Fix crash --- .../src/process_group_manager/processgroupmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp b/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp index 6510243f..bc9f21f4 100644 --- a/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp +++ b/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp @@ -590,7 +590,7 @@ inline void ProcessGroupManager::processGroupHandler(Graph& pg) { } } - if (GraphState::kUndefinedState == graph_state) { + if (GraphState::kUndefinedState == pg.getState()) { // at the moment graph is not running... // i.e. it is not in kInTransition, kAborting or kCancelled state // From 8141ef211532002fb6d075f116817c5c91c34f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 5 Mar 2026 11:30:08 +0100 Subject: [PATCH 57/72] Remove temporary schema file --- scripts/config_mapping/config.bzl | 2 +- scripts/config_mapping/integration_tests.py | 2 +- scripts/config_mapping/lifecycle_config.py | 11 +- .../basic_test/expected_output/hmcore.json | 2 +- .../tests/basic_test/input/lm_config.json | 12 +- .../input/lm_config.json | 2 +- .../empty_lm_config_test/input/lm_config.json | 12 +- .../expected_output/hmcore.json | 11 +- .../health_config_test/input/lm_config.json | 18 +- .../tests/lm_config_test/input/lm_config.json | 12 +- .../input/lm_config.json | 3 +- src/launch_manager_daemon/config/BUILD | 1 - .../config/config_schema/BUILD | 1 + .../config/s-core_launch_manager.schema.json | 498 ------------------ 14 files changed, 33 insertions(+), 554 deletions(-) create mode 100644 src/launch_manager_daemon/config/config_schema/BUILD delete mode 100644 src/launch_manager_daemon/config/s-core_launch_manager.schema.json diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl index 24826476..fead2e4e 100644 --- a/scripts/config_mapping/config.bzl +++ b/scripts/config_mapping/config.bzl @@ -86,7 +86,7 @@ gen_lifecycle_config = rule( doc = "Json file to convert. Note that the binary file will have the same name as the json (minus the suffix)", ), "schema": attr.label( - default=Label("//src/launch_manager_daemon/config:s-core_launch_manager.schema.json"), + default=Label("//src/launch_manager_daemon/config/config_schema:s-core_launch_manager.schema.json"), allow_single_file = [".json"], doc = "Json schema file to validate the input json against", ), diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index a4ca1742..75b929d0 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -12,7 +12,7 @@ ) script_dir = Path(__file__).parent -schema_path = script_dir.parent.parent / "src" / "launch_manager_daemon" / "config" / "s-core_launch_manager.schema.json" +schema_path = script_dir.parent.parent / "src" / "launch_manager_daemon" / "config" / "config_schema" / "s-core_launch_manager.schema.json" tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index c67853dd..0690ed2b 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -54,7 +54,7 @@ "alive_supervision" : { "evaluation_cycle": 0.5 }, - "watchdogs": {} + "watchdog": {} } """) @@ -152,8 +152,8 @@ def dict_merge_recursive(dict_a, dict_b): new_config["alive_supervision"] = dict_merge( merged_defaults["alive_supervision"], config.get("alive_supervision", {}) ) - new_config["watchdogs"] = dict_merge( - merged_defaults["watchdogs"], config.get("watchdogs", {}) + new_config["watchdog"] = dict_merge( + merged_defaults["watchdog"], config.get("watchdog", {}) ) for key in ("initial_run_target", "fallback_run_target"): @@ -351,9 +351,10 @@ def get_all_refProcessGroupStates(run_targets): ) } ] - for watchdog_name, watchdog_config in config.get("watchdogs", {}).items(): + + if watchdog_config := config.get("watchdog", {}): watchdog = {} - watchdog["shortName"] = watchdog_name + watchdog["shortName"] = "Watchdog" watchdog["deviceFilePath"] = watchdog_config["device_file_path"] watchdog["maxTimeout"] = sec_to_ms(watchdog_config["max_timeout"]) watchdog["deactivateOnShutdown"] = watchdog_config["deactivate_on_shutdown"] diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json index 88444ba2..15f488c6 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json @@ -3,7 +3,7 @@ "versionMinor": 0, "watchdogs": [ { - "shortName": "simple_watchdog", + "shortName": "Watchdog", "deviceFilePath": "/dev/watchdog", "maxTimeout": 2000, "deactivateOnShutdown": true, diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index 65b6326d..b35597f9 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -159,12 +159,10 @@ "alive_supervision" : { "evaluation_cycle": 0.5 }, - "watchdogs": { - "simple_watchdog": { - "device_file_path": "/dev/watchdog", - "max_timeout": 2, - "deactivate_on_shutdown": true, - "require_magic_close": false - } + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false } } \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json index a19c2cd6..217c9d66 100644 --- a/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json @@ -70,5 +70,5 @@ "alive_supervision" : { "evaluation_cycle": 0.123 }, - "watchdogs": {} + "watchdog": {} } \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json index 66be2556..b880105e 100644 --- a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json @@ -83,12 +83,10 @@ "alive_supervision" : { "evaluation_cycle": 0.5 }, - "watchdogs": { - "simple_watchdog": { - "device_file_path": "/dev/watchdog", - "max_timeout": 2, - "deactivate_on_shutdown": true, - "require_magic_close": false - } + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false } } \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json index 55313469..cb9148b3 100644 --- a/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json @@ -3,22 +3,13 @@ "versionMinor": 0, "watchdogs": [ { - "shortName": "simple_watchdog", + "shortName": "Watchdog", "deviceFilePath": "/dev/watchdog", "maxTimeout": 2000, "deactivateOnShutdown": true, "hasValueDeactivateOnShutdown": true, "requireMagicClose": false, "hasValueRequireMagicClose": true - }, - { - "shortName": "complex_watchdog", - "deviceFilePath": "/dev/watchdog2", - "maxTimeout": 1000, - "deactivateOnShutdown": false, - "hasValueDeactivateOnShutdown": true, - "requireMagicClose": true, - "hasValueRequireMagicClose": true } ], "config": [ diff --git a/scripts/config_mapping/tests/health_config_test/input/lm_config.json b/scripts/config_mapping/tests/health_config_test/input/lm_config.json index 679e1083..f86abac2 100644 --- a/scripts/config_mapping/tests/health_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/health_config_test/input/lm_config.json @@ -128,18 +128,10 @@ "alive_supervision" : { "evaluation_cycle": 0.123 }, - "watchdogs": { - "simple_watchdog": { - "device_file_path": "/dev/watchdog", - "max_timeout": 2, - "deactivate_on_shutdown": true, - "require_magic_close": false - }, - "complex_watchdog": { - "device_file_path": "/dev/watchdog2", - "max_timeout": 1, - "deactivate_on_shutdown": false, - "require_magic_close": true - } + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false } } \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json index e244503a..f825876f 100644 --- a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json @@ -182,12 +182,10 @@ "alive_supervision" : { "evaluation_cycle": 0.5 }, - "watchdogs": { - "simple_watchdog": { - "device_file_path": "/dev/watchdog", - "max_timeout": 2, - "deactivate_on_shutdown": true, - "require_magic_close": false - } + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false } } \ No newline at end of file diff --git a/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json b/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json index 149c5161..9fd06e96 100644 --- a/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json +++ b/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json @@ -17,6 +17,5 @@ "description": "Run Target with missing recovery action", "depends_on": [] } - }, - "initial_run_target": "Minimal" + } } \ No newline at end of file diff --git a/src/launch_manager_daemon/config/BUILD b/src/launch_manager_daemon/config/BUILD index 4be2d1aa..e4b6d642 100644 --- a/src/launch_manager_daemon/config/BUILD +++ b/src/launch_manager_daemon/config/BUILD @@ -1,5 +1,4 @@ exports_files([ - "s-core_launch_manager.schema.json", "lm_flatcfg.fbs", "lm_flatcfg_generated.h", ]) diff --git a/src/launch_manager_daemon/config/config_schema/BUILD b/src/launch_manager_daemon/config/config_schema/BUILD new file mode 100644 index 00000000..4a754d37 --- /dev/null +++ b/src/launch_manager_daemon/config/config_schema/BUILD @@ -0,0 +1 @@ +exports_files(["s-core_launch_manager.schema.json"]) \ No newline at end of file diff --git a/src/launch_manager_daemon/config/s-core_launch_manager.schema.json b/src/launch_manager_daemon/config/s-core_launch_manager.schema.json deleted file mode 100644 index fdc93b82..00000000 --- a/src/launch_manager_daemon/config/s-core_launch_manager.schema.json +++ /dev/null @@ -1,498 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "s-core_launch_manager.schema.json", - "title": "Configuration schema for the S-CORE Launch Manager", - "type": "object", - "$defs": { - "component_properties": { - "type": "object", - "description": "Defines a reusable type that captures the essential development-time characteristics of a software component.", - "properties": { - "binary_name": { - "type": "string", - "description": "Specifies the relative path of the executable file inside the directory defined by 'deployment_config.bin_dir'. The final executable path will be resolved as '{bin_dir}/{binary_name}'. Example values include simple filenames (e.g., 'test_app1') or subdirectory paths (e.g., 'bin/test_app1')." - }, - "application_profile": { - "type": "object", - "description": "Specifies the application profile that defines the runtime behavior and capabilities of this component.", - "properties": { - "application_type": { - "type": "string", - "enum": [ - "Native", - "Reporting", - "Reporting_And_Supervised", - "State_Manager" - ], - "description": "Specifies the level of integration between the component and the Launch Manager. 'Native': no integration with Launch Manager. 'Reporting': uses Launch Manager lifecycle APIs. 'Reporting_And_Supervised': uses lifecycle APIs and sends alive notifications. 'State_Manager': uses lifecycle APIs, sends alive notifications, and has permission to change the active Run Target." - }, - "is_self_terminating": { - "type": "boolean", - "description": "Indicates whether component is designed to terminate automatically once its planned tasks are completed (true), or remain running until explicitly requested to terminate by the Launch Manager (false)." - }, - "alive_supervision": { - "type": "object", - "description": "Specifies the configuration parameters used for alive monitoring of the component.", - "properties": { - "reporting_cycle": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the duration of the time interval used to verify that the component sends alive notifications, within the expected time frame." - }, - "failed_cycles_tolerance": { - "type": "integer", - "minimum": 0, - "description": "Defines the maximum number of consecutive reporting cycle failures (see 'reporting_cycle'). Once the number of failed cycles goes above maximum number, Launch Manager will trigger configured recovery action." - }, - "min_indications": { - "type": "integer", - "minimum": 0, - "description": "Specifies the minimum number of checkpoints that must be reported within each configured 'reporting_cycle'." - }, - "max_indications": { - "type": "integer", - "minimum": 0, - "description": "Specifies the maximum number of checkpoints that may be reported within each configured 'reporting_cycle'." - } - }, - "required": [], - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "depends_on": { - "type": "array", - "description": "Names of components that this component depends on. Each dependency must be initialized and reach its ready state before this component can start.", - "items": { - "type": "string", - "description": "Specifies the name of a component on which this component depends." - } - }, - "process_arguments": { - "type": "array", - "description": "Ordered list of command-line arguments passed to the component at startup.", - "items": { - "type": "string", - "description": "Single command-line argument token as a UTF-8 string; order is preserved." - } - }, - "ready_condition": { - "type": "object", - "description": "Specifies the set of conditions that mark when the component completes its initializing state and enters the ready state.", - "properties": { - "process_state": { - "type": "string", - "enum": [ - "Running", - "Terminated" - ], - "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." - } - }, - "required": [], - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "recovery_action": { - "type": "object", - "description": "Defines a reusable type that specifies which recovery actions should be executed when an error or failure occurs.", - "properties": { - "restart": { - "type": "object", - "description": "Specifies a recovery action that restarts the POSIX process associated with this component.", - "properties": { - "number_of_attempts": { - "type": "integer", - "minimum": 0, - "description": "Specifies the maximum number of restart attempts before the Launch Manager concludes that recovery cannot succeed." - }, - "delay_before_restart": { - "type": "number", - "minimum": 0, - "description": "Specifies the delay duration that Launch Manager shall wait before initiating a restart attempt." - } - }, - "required": [], - "additionalProperties": false - }, - "switch_run_target": { - "type": "object", - "description": "Specifies a recovery action that switches to a Run Target. This can be a different Run Target or the same one to retry activation of the current Run Target.", - "properties": { - "run_target": { - "type": "string", - "description": "Specifies the name of the Run Target that Launch Manager should switch to." - } - }, - "required": [], - "additionalProperties": false - } - }, - "oneOf": [ - { - "required": [ - "restart" - ] - }, - { - "required": [ - "switch_run_target" - ] - } - ], - "additionalProperties": false - }, - "deployment_config": { - "type": "object", - "description": "Defines a reusable type that contains the configuration parameters that are specific to a particular deployment environment or system setup.", - "properties": { - "ready_timeout": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the maximum time allowed for the component to reach its ready state. The timeout is measured from when the component process is created until the ready conditions specified in 'component_properties.ready_condition' are met." - }, - "shutdown_timeout": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the maximum allowed time for the component to terminate after it receives a SIGTERM signal from the Launch Manager. The timeout is measured from the moment the Launch Manager sends the SIGTERM signal, until the Operating System notifies the Launch Manager that the child process has terminated." - }, - "environmental_variables": { - "type": "object", - "description": "Defines the set of environment variables that will be passed to the component at startup.", - "additionalProperties": { - "type": "string", - "description": "Specifies the environment variable's value as a string. An empty string is allowed and represents an intentionally empty environment variable." - } - }, - "bin_dir": { - "type": "string", - "description": "Specifies the absolute filesystem path to the directory where component is installed." - }, - "working_dir": { - "type": "string", - "description": "Specifies the directory that will be used as the working directory for the component during execution." - }, - "ready_recovery_action": { - "allOf": [ - { - "$ref": "#/$defs/recovery_action" - }, - { - "properties": { - "restart": true - }, - "required": [ - "restart" - ], - "not": { - "required": [ - "switch_run_target" - ] - } - } - ], - "description": "Specifies the recovery action to execute when the component fails to reach its ready state within the configured timeout." - }, - "recovery_action": { - "allOf": [ - { - "$ref": "#/$defs/recovery_action" - }, - { - "properties": { - "switch_run_target": true - }, - "required": [ - "switch_run_target" - ], - "not": { - "required": [ - "restart" - ] - } - } - ], - "description": "Specifies the recovery action to execute when the component malfunctions after reaching its ready state." - }, - "sandbox": { - "type": "object", - "description": "Specifies the sandbox configuration parameters that isolate and constrain the component's runtime execution.", - "properties": { - "uid": { - "type": "integer", - "minimum": 0, - "description": "Specifies the POSIX user ID (UID) under which this component executes." - }, - "gid": { - "type": "integer", - "minimum": 0, - "description": "Specifies the primary POSIX group ID (GID) under which this component executes." - }, - "supplementary_group_ids": { - "type": "array", - "description": "Specifies the list of supplementary POSIX group IDs (GIDs) assigned to this component.", - "items": { - "type": "integer", - "minimum": 0, - "description": "Single supplementary POSIX group ID (GID)" - } - }, - "security_policy": { - "type": "string", - "description": "Specifies the security policy or confinement profile name (such as an SELinux or AppArmor profile) assigned to the component." - }, - "scheduling_policy": { - "type": "string", - "description": "Specifies the scheduling policy applied to the component's initial thread. Supported values correspond to OS-defined policies (e.g., FIFO, RR, OTHER).", - "anyOf": [ - { - "enum": [ - "SCHED_FIFO", - "SCHED_RR", - "SCHED_OTHER" - ] - }, - { - "type": "string" - } - ] - }, - "scheduling_priority": { - "type": "integer", - "description": "Specifies the scheduling priority applied to the component's initial thread." - }, - "max_memory_usage": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "Specifies the maximum amount of memory, in bytes, that the component is permitted to use during runtime." - }, - "max_cpu_usage": { - "type": "integer", - "exclusiveMinimum": 0, - "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage of total CPU capacity." - } - }, - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "run_target": { - "type": "object", - "description": "Defines a reusable type that specifies the configuration parameters for a Run Target.", - "properties": { - "description": { - "type": "string", - "description": "User-defined description of the configured Run Target." - }, - "depends_on": { - "type": "array", - "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", - "items": { - "type": "string", - "description": "Name of a component or Run Target that this Run Target depends on." - } - }, - "transition_timeout": { - "type": "number", - "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", - "exclusiveMinimum": 0 - }, - "recovery_action": { - "allOf": [ - { - "$ref": "#/$defs/recovery_action" - }, - { - "properties": { - "switch_run_target": true - }, - "required": [ - "switch_run_target" - ], - "not": { - "required": [ - "restart" - ] - } - } - ], - "description": "Specifies the recovery action to execute when a component assigned to this Run Target fails." - } - }, - "required": [ - "recovery_action" - ], - "additionalProperties": false - }, - "alive_supervision": { - "type": "object", - "description": "Defines a reusable type that contains the configuration parameters for alive supervision", - "properties": { - "evaluation_cycle": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Specifies the length of the time window used to assess incoming alive supervision reports." - } - }, - "required": [], - "additionalProperties": false - }, - "watchdog": { - "type": "object", - "description": "Defines a reusable type that contains the configuration parameters for the external watchdog.", - "properties": { - "device_file_path": { - "type": "string", - "description": "Path to the external watchdog device file (e.g., /dev/watchdog)." - }, - "max_timeout": { - "type": "number", - "minimum": 0, - "description": "Specifies the maximum timeout value that the Launch Manager will configure on the external watchdog during startup. The external watchdog uses this timeout as the deadline for receiving periodic alive reports from the Launch Manager." - }, - "deactivate_on_shutdown": { - "type": "boolean", - "description": "Specifies whether the Launch Manager disables the external watchdog during shutdown. When set to true, the watchdog is deactivated; when false, it remains active." - }, - "require_magic_close": { - "type": "boolean", - "description": "When true, the Launch Manager will perform a defined shutdown sequence to inform the external watchdog that the shutdown is intentional and to prevent a watchdog-initiated reset." - } - }, - "required": [], - "additionalProperties": false - } - }, - "properties": { - "schema_version": { - "type": "integer", - "description": "Specifies the schema version number that the Launch Manager uses to determine how to parse and validate this configuration file.", - "enum": [ - 1 - ] - }, - "defaults": { - "type": "object", - "description": "Specifies default configuration values that components and Run Targets inherit unless they provide their own overriding values.", - "properties": { - "component_properties": { - "description": "Specifies default component property values applied to all components unless overridden in individual component definitions.", - "$ref": "#/$defs/component_properties" - }, - "deployment_config": { - "description": "Specifies default deployment configuration values applied to all components unless overridden in individual component definitions.", - "$ref": "#/$defs/deployment_config" - }, - "run_target": { - "description": "Specifies default Run Target configuration values applied to all Run Targets unless overridden in individual Run Target definitions.", - "$ref": "#/$defs/run_target" - }, - "alive_supervision": { - "description": "Specifies default alive supervision configuration values that are used unless a global 'alive_supervision' configuration is defined at the root level.", - "$ref": "#/$defs/alive_supervision" - }, - "watchdog": { - "description": "Specifies default watchdog configuration values applied to all watchdogs unless overridden in individual watchdog definitions.", - "$ref": "#/$defs/watchdog" - } - }, - "required": [], - "additionalProperties": false - }, - "components": { - "type": "object", - "description": "Defines software components managed by the Launch Manager, where each property name is a unique component identifier and its value contains the component's configuration.", - "patternProperties": { - "^[a-zA-Z0-9_-]+$": { - "type": "object", - "description": "Specifies an individual component's configuration properties and deployment settings.", - "properties": { - "description": { - "type": "string", - "description": "A human-readable description of the component's purpose." - }, - "component_properties": { - "description": "Specifies component properties for this component; any properties not specified here are inherited from 'defaults.component_properties'.", - "$ref": "#/$defs/component_properties" - }, - "deployment_config": { - "description": "Specifies deployment configuration for this component; any properties not specified here are inherited from 'defaults.deployment_config'.", - "$ref": "#/$defs/deployment_config" - } - }, - "required": [], - "additionalProperties": false - } - }, - "required": [], - "additionalProperties": false - }, - "run_targets": { - "type": "object", - "description": "Defines Run Targets representing different operational modes of the system, where each property name is a unique Run Target identifier and its value contains the Run Target's configuration.", - "patternProperties": { - "^[a-zA-Z0-9_-]+$": { - "$ref": "#/$defs/run_target" - } - }, - "required": [], - "additionalProperties": false - }, - "initial_run_target": { - "type": "string", - "description": "Specifies the initial Run Target name that the Launch Manager activates during startup sequence. This name shall match a Run Target defined in 'run_targets'." - }, - "fallback_run_target": { - "type": "object", - "description": "Defines the fallback Run Target configuration that the Launch Manager activates when all recovery attempts have been exhausted. This Run Target does not include a recovery_action property.", - "properties": { - "description": { - "type": "string", - "description": "A human-readable description of the fallback Run Target." - }, - "depends_on": { - "type": "array", - "description": "Names of components and Run Targets that must be activated when this Run Target is activated.", - "items": { - "type": "string", - "description": "Name of a component or Run Target that this Run Target depends on." - } - }, - "transition_timeout": { - "type": "number", - "description": "Time limit for the Run Target transition. If this limit is exceeded, the transition is considered as failed.", - "exclusiveMinimum": 0 - } - }, - "required": [], - "additionalProperties": false - }, - "alive_supervision": { - "description": "Defines alive supervision configuration parameters used to monitor component health. This configuration overrides 'defaults.alive_supervision' if specified.", - "$ref": "#/$defs/alive_supervision" - }, - "watchdogs": { - "type": "object", - "description": "Defines external watchdog devices used by the Launch Manager, where each property name is a unique watchdog identifier and its value contains the watchdog's configuration.", - "patternProperties": { - "^[a-zA-Z0-9_-]+$": { - "$ref": "#/$defs/watchdog" - } - }, - "required": [], - "additionalProperties": false - } - }, - "description": "Specifies the structure and valid values for the Launch Manager configuration file, which defines managed components, run targets, and recovery behaviors.", - "required": [ - "schema_version" - ], - "additionalProperties": false -} From 28a9e3d8bccf84b4df0b0ad9d455f5901fdc6908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 5 Mar 2026 12:20:40 +0100 Subject: [PATCH 58/72] Update readme file --- scripts/config_mapping/Readme.md | 42 ++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index d4ac9be8..dc04c710 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -28,13 +28,13 @@ gen_lifecycle_config( ## Python ``` -python3 lifecycle_config.py -o +python3 lifecycle_config.py -o --schema ``` -If you want to **only** validate the configuration without generating any output: +If you want to **only** validate the configuration against its schema without generating any output: ``` -python3 lifecycle_config.py --validate +python3 lifecycle_config.py --schema --validate ``` # Running Tests @@ -63,7 +63,9 @@ Each RunTarget will be mapped to a ProcessGroupState with the same name. For example, RunTarget `Minimal` will result in a ProcessGroupState called `MainPG/Minimal`. The ProcessGroupState will contain all the processes that would be started as part of the associated RunTarget. -The LaunchManager will startup up `MainPG/Startup` by default. Therefore, we require for now that `initial_run_target` must be set to `Startup`. +The LaunchManager will currently always startup up `MainPG/Startup` as initial state. +Therefore, we require for now that `initial_run_target` must be set to `Startup`. +This is ensured as part of the translation, configs with `initial_run_target` not equal to `Startup` are rejected. ## Mapping of Components to Processes @@ -79,35 +81,39 @@ Every Component can only have a single deployment config, therefore the mapped P The ReadyCondition of a Component is mapped to an execution dependency between two processes. If Component A has ReadyCondition `process_state:Running` and Component B depends on Component A. Then the ReadyCondition of Component A is mapped to `Component B depends on Component A in State Running`. -For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that are not directly assigned to a RunTarget. Otherwise, this ReadyCondition cannot be mapped to an execution dependency. +For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that have at least one other Component depending on it. Otherwise, this ReadyCondition cannot be mapped to an execution dependency. ## Mapping of Recovery Actions The only supported RecoveryAction during startup of a Component is the restart of a Component. This RecoveryAction is mapped to the `restartAttempts` parameter in the old configuration. -The RecoveryAction after component startup (parameter `components//deployment_config/recovery_action`) as well as the RecoveryAction for RunTargets (parameter `run_targets//recovery_action`) are currently not supported. - -The `run_targets/final_recovery_action` RecoveryAction will be mapped to the `ModeGroup/recoveryMode_name` parameter. This will initiate a transition to the target ProcessGroupState/RunTarget when a process crashes at runtime or a supervision fails. We assume that this transition must not fail. +For failures after Component startup the only currently supported RecoveryAction is switching to the `fallback_run_target`. +The `fallback_run_target` is mapped to a ProcessGroupState `MainPG/fallback_run_target` and this state will be configured as the recovery state (`ModeGroup/recoveryMode_name`). +This will initiate a transition to the target ProcessGroupState/RunTarget when a process crashes at runtime or a supervision fails. We assume that this transition must not fail. ## Mapping of Alive Supervision -For each Component with application_type `REPORTING_AND_SUPERVISED` or `STATE_MANAGER`, we will create an Alive Supervision configuration in the old configuration format. There is a 1:1 mapping from the `component_properties/application_profile/alive_supervision` parameters to the old configuration Alive Supervision structure. +For each Component with application_type `Reporting_And_Supervised` or `State_Manager`, we will create an Alive Supervision configuration. There is a 1:1 mapping from the `component_properties/application_profile/alive_supervision` parameters to the old configuration Alive Supervision structure. ## Mapping of Watchdog Configuration +The new configuration format allows to configure a single watchdog. There is simple 1:1 mapping to the old watchdog configuration format. + ## Known Limitations +### Component + * The sandbox parameters `max_memory_usage` and `max_cpu_usage` are currently not supported. -* The initial RunTarget must be named `Startup` and the `initial_run_target` must be configured to `Startup`. -* For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that are not directly assigned to a RunTarget -* The `ready_recovery_action` only supports the RecoveryAction of type `restart` -* The parameters `components//deployment_config/recovery_action` and `run_targets//recovery_action` are currently not supported. Only the global `final_recovery_action` is supported -* The parameter `run_targets//transition_timeout` is currently not supported +* For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that have at least one Component depending on it +* The `ready_recovery_action` only supports the RecoveryAction of type `restart`. The parameter `delay_before_restart` is currently not supported. +* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target` +* The `ready_timeout` is used as the timeout until process state Running is reached, even in case the ReadyCondition is `process_state:Terminated` +* The parameter `deployment_config/working_dir` is currently ont supported +### Run Target -Open topics: +* The initial RunTarget must be named `Startup` and the `initial_run_target` must be configured to `Startup`. +* The parameter `run_targets//transition_timeout` is currently not supported +* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target` -* What if an object is explicitly set to {} in the config? Will this overwrite the default to None? -* What about supervision and state manager? -* What is the default ready condition? From e4569e7b616d95f401ce3c1b3517b72d47b00b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 6 Mar 2026 10:25:01 +0100 Subject: [PATCH 59/72] Clarify that certain attributes will be ignored --- scripts/config_mapping/Readme.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index dc04c710..b6f8f682 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -103,17 +103,17 @@ The new configuration format allows to configure a single watchdog. There is sim ### Component -* The sandbox parameters `max_memory_usage` and `max_cpu_usage` are currently not supported. -* For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that have at least one Component depending on it -* The `ready_recovery_action` only supports the RecoveryAction of type `restart`. The parameter `delay_before_restart` is currently not supported. -* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target` -* The `ready_timeout` is used as the timeout until process state Running is reached, even in case the ReadyCondition is `process_state:Terminated` -* The parameter `deployment_config/working_dir` is currently ont supported +* The sandbox parameters `max_memory_usage` and `max_cpu_usage` are currently not supported and will be ignored. +* For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that have at least one Component depending on it. +* The `ready_recovery_action` only supports the RecoveryAction of type `restart`. The parameter `delay_before_restart` is currently not supported and will be ignored. +* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target`. +* The `ready_timeout` is used as the timeout until process state Running is reached, even in case the ReadyCondition is `process_state:Terminated`. +* The parameter `deployment_config/working_dir` is currently not supported and will be ignored. ### Run Target * The initial RunTarget must be named `Startup` and the `initial_run_target` must be configured to `Startup`. -* The parameter `run_targets//transition_timeout` is currently not supported -* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target` +* The parameter `run_targets//transition_timeout` is currently not supported and will be ignored. +* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target`. From abf6b284dd816448014db20d85c5d1b58ee25638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 6 Mar 2026 13:44:17 +0100 Subject: [PATCH 60/72] Use lowercase name for watchdog --- scripts/config_mapping/lifecycle_config.py | 2 +- .../config_mapping/tests/basic_test/expected_output/hmcore.json | 2 +- .../tests/empty_lm_config_test/expected_output/hmcore.json | 2 +- .../tests/health_config_test/expected_output/hmcore.json | 2 +- .../tests/lm_config_test/expected_output/hmcore.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 0690ed2b..89edd6fc 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -354,7 +354,7 @@ def get_all_refProcessGroupStates(run_targets): if watchdog_config := config.get("watchdog", {}): watchdog = {} - watchdog["shortName"] = "Watchdog" + watchdog["shortName"] = "watchdog" watchdog["deviceFilePath"] = watchdog_config["device_file_path"] watchdog["maxTimeout"] = sec_to_ms(watchdog_config["max_timeout"]) watchdog["deactivateOnShutdown"] = watchdog_config["deactivate_on_shutdown"] diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json index 15f488c6..f06adf40 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json @@ -3,7 +3,7 @@ "versionMinor": 0, "watchdogs": [ { - "shortName": "Watchdog", + "shortName": "watchdog", "deviceFilePath": "/dev/watchdog", "maxTimeout": 2000, "deactivateOnShutdown": true, diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json index 88444ba2..f06adf40 100644 --- a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json @@ -3,7 +3,7 @@ "versionMinor": 0, "watchdogs": [ { - "shortName": "simple_watchdog", + "shortName": "watchdog", "deviceFilePath": "/dev/watchdog", "maxTimeout": 2000, "deactivateOnShutdown": true, diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json index cb9148b3..cad238fb 100644 --- a/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json @@ -3,7 +3,7 @@ "versionMinor": 0, "watchdogs": [ { - "shortName": "Watchdog", + "shortName": "watchdog", "deviceFilePath": "/dev/watchdog", "maxTimeout": 2000, "deactivateOnShutdown": true, diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json index 88444ba2..f06adf40 100644 --- a/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json @@ -3,7 +3,7 @@ "versionMinor": 0, "watchdogs": [ { - "shortName": "simple_watchdog", + "shortName": "watchdog", "deviceFilePath": "/dev/watchdog", "maxTimeout": 2000, "deactivateOnShutdown": true, From cba37094a42fe539fa824db0b15d45a2f5fa5adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Fri, 6 Mar 2026 14:26:32 +0100 Subject: [PATCH 61/72] Run config tests via bazel --- scripts/config_mapping/BUILD | 24 +++++++++++++++++ scripts/config_mapping/Readme.md | 13 +-------- scripts/config_mapping/integration_tests.py | 2 +- scripts/config_mapping/unit_tests.py | 30 +++++++++------------ 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD index 17e0aea2..516c0835 100644 --- a/scripts/config_mapping/BUILD +++ b/scripts/config_mapping/BUILD @@ -1,5 +1,6 @@ load("@score_lifecycle_pip//:requirements.bzl", "requirement") +load("@score_tooling//:defs.bzl", "score_py_pytest") # Example Usage # load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") @@ -14,3 +15,26 @@ py_binary( deps = [requirement("jsonschema")], visibility = ["//visibility:public"], ) + +filegroup( + name = "integration_test_files", + srcs = glob(["tests/**/*/*.json"]), +) + +score_py_pytest( + name = "lifecycle_config_tests", + srcs = [ + "unit_tests.py", + "integration_tests.py", + ], + args = [ + ], + data = [ + ":lifecycle_config.py", + "//src/launch_manager_daemon/config/config_schema:s-core_launch_manager.schema.json", + ":integration_test_files" + ], + tags = ["manual"], + deps = [requirement("pytest"), requirement("jsonschema")], + +) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index b6f8f682..e257b8ce 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -39,19 +39,8 @@ python3 lifecycle_config.py --schema - # Running Tests -You may want to use the virtual environment: - -```bash -python3 -m venv myvenv -. myvenv/bin/activate -pip3 install -r requirements.txt -``` - -Execute tests: - ```bash -pytest unit_tests.py -pytest integration_tests.py +bazel test //scripts/config_mapping:lifecycle_config_tests ``` # Mapping Details diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 75b929d0..0f0c3da9 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -4,7 +4,7 @@ import shutil from pathlib import Path import filecmp -from lifecycle_config import ( +from scripts.config_mapping.lifecycle_config import ( SUCCESS, SCHEMA_VALIDATION_DEPENDENCY_ERROR, SCHEMA_VALIDATION_FAILURE, diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py index 037478fe..1c993daf 100644 --- a/scripts/config_mapping/unit_tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 - -from lifecycle_config import preprocess_defaults +from scripts.config_mapping.lifecycle_config import preprocess_defaults import json - def test_preprocessing_basic(): """ Basic smoketest for the preprocess_defaults function, to ensure that defaults are being applied and overridden correctly. @@ -32,7 +30,7 @@ def test_preprocessing_basic(): "alive_supervision": { "evaluation_cycle": 0.5 }, - "watchdogs": {} + "watchdog": {} }""") config = json.loads("""{ @@ -82,13 +80,11 @@ def test_preprocessing_basic(): "alive_supervision": { "evaluation_cycle": 0.1 }, - "watchdogs": { - "simple_watchdog": { - "device_file_path": "/dev/watchdog", - "max_timeout": 2, - "deactivate_on_shutdown": true, - "require_magic_close": false - } + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false } }""") @@ -130,13 +126,11 @@ def test_preprocessing_basic(): "alive_supervision": { "evaluation_cycle": 0.1 }, - "watchdogs": { - "simple_watchdog": { - "device_file_path": "/dev/watchdog", - "max_timeout": 2, - "deactivate_on_shutdown": true, - "require_magic_close": false - } + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false } }""") From 4e968b97ba08352ed85393828d77e3fa549a793d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 9 Mar 2026 08:57:58 +0100 Subject: [PATCH 62/72] Define central lifecycle bazel rules --- defs.bzl | 19 +++++++++++++++++++ examples/BUILD | 4 ++-- scripts/config_mapping/BUILD | 7 ------- scripts/config_mapping/Readme.md | 6 +++--- scripts/config_mapping/config.bzl | 6 +++--- tests/integration/smoke/BUILD | 4 ++-- 6 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 defs.bzl diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 00000000..278a2ca0 --- /dev/null +++ b/defs.bzl @@ -0,0 +1,19 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Unified entrypoint for lifecycle Bazel macros & rules.""" + +# --- Launch Manager Configuration --- +load("//scripts/config_mapping:config.bzl", _launch_manager_config = "launch_manager_config") + +launch_manager_config = _launch_manager_config \ No newline at end of file diff --git a/examples/BUILD b/examples/BUILD index 8f3c5064..230b9906 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -1,7 +1,7 @@ -load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") +load("//:defs.bzl", "launch_manager_config") load(":run_examples.bzl", "run_examples") -gen_lifecycle_config( +launch_manager_config( name ="example_config", config="//examples/config:lifecycle_demo_config") diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD index 516c0835..ac021217 100644 --- a/scripts/config_mapping/BUILD +++ b/scripts/config_mapping/BUILD @@ -2,13 +2,6 @@ load("@score_lifecycle_pip//:requirements.bzl", "requirement") load("@score_tooling//:defs.bzl", "score_py_pytest") -# Example Usage -# load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") -# exports_files(["lm_config.json"]) -# gen_lifecycle_config( -# name ="example_config_gen", -# config="//scripts/config_mapping:lm_config.json") - py_binary( name = "lifecycle_config", srcs = ["lifecycle_config.py"], diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md index e257b8ce..de6ad3d5 100644 --- a/scripts/config_mapping/Readme.md +++ b/scripts/config_mapping/Readme.md @@ -10,16 +10,16 @@ Providing a json file using the new configuration format as input, the script wi ## Bazel -The bazel function `gen_lifecycle_config` handles the translation of the new configuration format into the old configuration format and also does the subsequent compilation to flatbuffer files. +The bazel function `launch_manager_config` handles the translation of the new configuration format into the old configuration format and also does the subsequent compilation to flatbuffer files. ```python -load("@score_lifecycle_health//scripts/config_mapping:config.bzl", "gen_lifecycle_config") +load("@score_lifecycle_health//:defs.bzl", "launch_manager_config") # This is your launch manager configuration in the new format exports_files(["lm_config.json"]) # Afterwards, you can refer to the generated flatbuffer files with :example_config_gen -gen_lifecycle_config( +launch_manager_config( name ="example_config_gen", config="//scripts/config_mapping:lm_config.json" ) diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl index fead2e4e..e3cbfccb 100644 --- a/scripts/config_mapping/config.bzl +++ b/scripts/config_mapping/config.bzl @@ -1,4 +1,4 @@ -def _gen_lifecycle_config_impl(ctx): +def _launch_manager_config_impl(ctx): config = ctx.file.config schema = ctx.file.schema script = ctx.executable.script @@ -77,8 +77,8 @@ def _gen_lifecycle_config_impl(ctx): return DefaultInfo(files = depset([gen_dir_flatbuffer]), runfiles = rf) -gen_lifecycle_config = rule( - implementation = _gen_lifecycle_config_impl, +launch_manager_config = rule( + implementation = _launch_manager_config_impl, attrs = { "config": attr.label( allow_single_file = [".json"], diff --git a/tests/integration/smoke/BUILD b/tests/integration/smoke/BUILD index edda97da..01457b5d 100644 --- a/tests/integration/smoke/BUILD +++ b/tests/integration/smoke/BUILD @@ -18,8 +18,8 @@ exports_files( ["lm_demo.json"], visibility = ["//examples:__subpackages__"]) -load("//scripts/config_mapping:config.bzl", "gen_lifecycle_config") -gen_lifecycle_config( +load("//:defs.bzl", "launch_manager_config") +launch_manager_config( name ="lm_smoketest_config", config="//tests/integration/smoke:lifecycle_smoketest.json", flatbuffer_out_dir="etc" From 821f0c81d596edb1f4f66c2f41c41705a556f815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 9 Mar 2026 15:32:48 +0100 Subject: [PATCH 63/72] Add progress message to bazel function --- scripts/config_mapping/config.bzl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl index e3cbfccb..14a97f00 100644 --- a/scripts/config_mapping/config.bzl +++ b/scripts/config_mapping/config.bzl @@ -11,7 +11,9 @@ def _launch_manager_config_impl(ctx): inputs = [config, schema], outputs = [gen_dir_json], tools = [script], + mnemonic = "LifecycleJsonConfigGeneration", executable = script, + progress_message = "generating Launch Manager config from {}".format(config.short_path), arguments = [ config.path, "--schema", schema.path, @@ -64,7 +66,9 @@ def _launch_manager_config_impl(ctx): hm_schema=hm_schema.path, flatc=flatc.path ), - arguments = [] + arguments = [], + mnemonic = "LaunchManagerFlatbufferConfigGeneration", + progress_message = "compiling generated Launch Manager configs in {} to flatbuffer files in {}".format(gen_dir_json.short_path, gen_dir_flatbuffer.short_path) ) rf = ctx.runfiles( From f05f28a9386bc4fbc691f3807b3372c18326bf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 9 Mar 2026 16:02:37 +0100 Subject: [PATCH 64/72] Make pip environment a proper dependency This environment is used in the public bazel function for config generation --- MODULE.bazel | 2 +- scripts/config_mapping/lifecycle_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 5b5d4690..b1fba7ae 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -119,7 +119,7 @@ python.toolchain( use_repo(python) # Python pip dependencies -pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True) +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( hub_name = "score_lifecycle_pip", python_version = PYTHON_VERSION, diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 89edd6fc..2ee87147 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -651,7 +651,7 @@ def schema_validation(json_input, schema): print("Schema Validation successful") return True except ValidationError as err: - print(err, file=sys.stderr) + print(err.message, file=sys.stderr) return False # Possible exit codes returned from this script From adad974873d860f36535071651097159b06cf5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 10 Mar 2026 08:45:30 +0100 Subject: [PATCH 65/72] Improve error message --- scripts/config_mapping/lifecycle_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 2ee87147..d8645f41 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -59,7 +59,7 @@ """) def report_error(message): - print(message, file=sys.stderr) + print(f"Error: {message}", file=sys.stderr) # There are various dictionaries in the config where only a single entry is allowed. # We do not want to merge the defaults with the user specified values for these dictionaries. @@ -446,7 +446,7 @@ def get_process_dependencies( # If the dependency is not a component, it must be a run target if dependency_name not in config["run_targets"]: raise ValueError( - f"Run target depends on unknown run target '{dependency_name}'." + f"Run target depends on unknown run target or component '{dependency_name}'." ) if dependency_name in ancestors_run_targets: path = format_dependency_path( @@ -651,7 +651,7 @@ def schema_validation(json_input, schema): print("Schema Validation successful") return True except ValidationError as err: - print(err.message, file=sys.stderr) + print(f"Error: Schema validation failed with error: {err.message}", file=sys.stderr) return False # Possible exit codes returned from this script From c8fcb3b565a04d5d03704bdd27567cede0b7446d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 10 Mar 2026 15:05:02 +0100 Subject: [PATCH 66/72] Remove obsolete config scripts --- examples/config/gen_common_cfg.py | 19 - examples/config/gen_health_monitor_cfg.py | 338 ----------- .../config/gen_health_monitor_process_cfg.py | 90 --- examples/config/gen_launch_manager_cfg.py | 530 ------------------ examples/config/hmcore.json | 11 - 5 files changed, 988 deletions(-) delete mode 100644 examples/config/gen_common_cfg.py delete mode 100644 examples/config/gen_health_monitor_cfg.py delete mode 100644 examples/config/gen_health_monitor_process_cfg.py delete mode 100644 examples/config/gen_launch_manager_cfg.py delete mode 100644 examples/config/hmcore.json diff --git a/examples/config/gen_common_cfg.py b/examples/config/gen_common_cfg.py deleted file mode 100644 index 815d7f0b..00000000 --- a/examples/config/gen_common_cfg.py +++ /dev/null @@ -1,19 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -def get_process_index_range(process_count: int, process_group_index: int): - # Every ProcessGroup gets the same number of processes - # The Process Index is a globally unique increasing number - return range( - process_group_index * process_count, - (process_group_index * process_count) + process_count, - ) diff --git a/examples/config/gen_health_monitor_cfg.py b/examples/config/gen_health_monitor_cfg.py deleted file mode 100644 index f79f6a30..00000000 --- a/examples/config/gen_health_monitor_cfg.py +++ /dev/null @@ -1,338 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -import argparse -from pathlib import Path -import os -import json -from gen_common_cfg import get_process_index_range - - -def get_process(index: int, process_group: str): - return ( - """ -{ - "index": """ - + str(index) - + """, - "shortName": "demo_application""" - + str(index) - + """", - "identifier": "demo_app""" - + str(index) - + "_" - + process_group - + '''", - "processType": "REGULAR_PROCESS", - "refProcessGroupStates": [ - { - "identifier": "''' - + process_group - + """/Startup" - } - ], - "processExecutionErrors": [ - { - "processExecutionError": 1 - } - ] -} -""" - ) - - -def get_monitor_interfaces(index: int, process_group: str): - return ( - """ -{ - "instanceSpecifier": "demo/demo_application""" - + str(index) - + """/Port1", - "processShortName": "demo_application""" - + str(index) - + """", - "portPrototype": "Port1", - "interfacePath": "demo_application_""" - + str(index) - + "_" - + process_group - + """", - "refProcessIndex": """ - + str(index) - + """, - "permittedUid": 0 -} -""" - ) - - -def get_checkpoints(index: int): - # Every demo app has three checkpoints - return [ - """ -{ - "shortName": "Checkpoint""" - + str(index) - + "_1" - + """", - "checkpointId": 1, - "refInterfaceIndex": """ - + str(index) - + """ -}""" - ] - - -def get_alive_supervisions(index: int, process_group: str): - # Every demo app has three checkpoints and the first checkpoint is used for alive supervision - checkpointIdx = index * 1 - return ( - """ -{ - "ruleContextKey": "AliveSupervision""" - + str(index) - + """", - "refCheckPointIndex": """ - + str(checkpointIdx) - + """, - "aliveReferenceCycle": 100.0, - "minAliveIndications": 1, - "maxAliveIndications": 3, - "isMinCheckDisabled": false, - "isMaxCheckDisabled": false, - "failedSupervisionCyclesTolerance": 1, - "refProcessIndex": """ - + str(index) - + ''', - "refProcessGroupStates": [ - { - "identifier": "''' - + process_group - + """/Startup" - } - ] -} -""" - ) - - -def get_local_supervisions(index: int): - return ( - """ -{ - "ruleContextKey": "LocalSupervision""" - + str(index) - + """", - "infoRefInterfacePath": "demo_application_""" - + str(index) - + """", - "hmRefAliveSupervision": [ - { - "refAliveSupervisionIdx": """ - + str(index) - + """ - } - ] -} -""" - ) - - -def get_global_supervisions( - process_count: int, process_group_index: int, process_group: str -): - localSupervisionRefs = [] - processRefs = [] - for i in get_process_index_range(process_count, process_group_index): - localSupervisionRefs.append( - json.loads( - """ -{ - "refLocalSupervisionIndex": """ - + str(i) - + """ -}""" - ) - ) - processRefs.append( - json.loads( - """ -{ - "index": """ - + str(i) - + """ -}""" - ) - ) - - globalSupervisions = json.loads( - """ -{ - "ruleContextKey": "GlobalSupervision_""" - + process_group - + '''", - "isSeverityCritical": false, - "localSupervision": [], - "refProcesses": [], - "refProcessGroupStates": [ - { - "identifier": "''' - + process_group - + """/Startup" - } - ] -} -""" - ) - globalSupervisions["localSupervision"].extend(localSupervisionRefs) - globalSupervisions["refProcesses"].extend(processRefs) - return json.dumps(globalSupervisions) - - -def get_recovery_notifications( - process_count: int, process_group_index: int, process_group: str -): - return ( - """ -{ - "shortName" : "RecoveryNotification_""" - + process_group - + '''", - "recoveryNotificationTimeout" : 4000.0, - "processGroupMetaModelIdentifier" : "''' - + process_group - + """/Recovery", - "refGlobalSupervisionIndex" : """ - + str(process_group_index) - + """, - "instanceSpecifier" : "", - "shouldFireWatchdog" : false -} -""" - ) - - -def gen_health_monitor_cfg_for_process_group( - config, process_count: int, process_group: str, process_group_index: int -): - processes = [] - monitorInterfaces = [] - checkpoints = [] - hmAliveSupervisions = [] - hmLocalSupervisions = [] - hmGlobalSupervision = [] - hmRecoveryNotifications = [] - - for process_index in get_process_index_range(process_count, process_group_index): - print(f"process Index {process_index} for FG {process_group}") - processes.append(json.loads(get_process(process_index, process_group))) - monitorInterfaces.append( - json.loads(get_monitor_interfaces(process_index, process_group)) - ) - - for cp in get_checkpoints(process_index): - checkpoints.append(json.loads(cp)) - - hmAliveSupervisions.append( - json.loads(get_alive_supervisions(process_index, process_group)) - ) - hmLocalSupervisions.append(json.loads(get_local_supervisions(process_index))) - - hmGlobalSupervision.append( - json.loads( - get_global_supervisions(process_count, process_group_index, process_group) - ) - ) - hmRecoveryNotifications.append( - json.loads( - get_recovery_notifications( - process_count, process_group_index, process_group - ) - ) - ) - - config["process"].extend(processes) - config["hmMonitorInterface"].extend(monitorInterfaces) - config["hmSupervisionCheckpoint"].extend(checkpoints) - config["hmAliveSupervision"].extend(hmAliveSupervisions) - config["hmLocalSupervision"].extend(hmLocalSupervisions) - config["hmGlobalSupervision"].extend(hmGlobalSupervision) - config["hmRecoveryNotification"].extend(hmRecoveryNotifications) - return config - - -def gen_health_monitor_cfg(process_count: int, process_groups: list): - config = json.loads( - """ -{ - "versionMajor": 8, - "versionMinor": 0, - "process": [], - "hmMonitorInterface": [], - "hmSupervisionCheckpoint": [], - "hmAliveSupervision": [], - "hmDeadlineSupervision": [], - "hmLogicalSupervision": [], - "hmLocalSupervision": [], - "hmGlobalSupervision": [], - "hmRecoveryNotification": [] -} -""" - ) - - for i in range(0, len(process_groups)): - gen_health_monitor_cfg_for_process_group( - config, process_count, process_groups[i], i - ) - - return config - - -if __name__ == "__main__": - my_parser = argparse.ArgumentParser() - my_parser.add_argument( - "-c", - "--cppprocesses", - action="store", - type=int, - required=True, - help="Number of C++ processes", - ) - my_parser.add_argument( - "-r", - "--rustprocesses", - action="store", - type=int, - required=True, - help="Number of Rust processes", - ) - my_parser.add_argument( - "-p", - "--process_groups", - nargs="+", - help="Name of a Process Group", - required=True, - ) - my_parser.add_argument( - "-o", "--out", action="store", type=Path, required=True, help="Output directory" - ) - args = my_parser.parse_args() - - cfg_out_path = os.path.join(args.out, f"hm_demo.json") - with open(cfg_out_path, "w") as f: - json.dump( - gen_health_monitor_cfg( - args.cppprocesses + args.rustprocesses, args.process_groups - ), - f, - indent=4, - ) diff --git a/examples/config/gen_health_monitor_process_cfg.py b/examples/config/gen_health_monitor_process_cfg.py deleted file mode 100644 index 357cb7e4..00000000 --- a/examples/config/gen_health_monitor_process_cfg.py +++ /dev/null @@ -1,90 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -import argparse -from pathlib import Path -import os -from gen_common_cfg import get_process_index_range - - -def gen_health_monitor_process_cfg(index: int, process_group: str): - config = ( - """ -{ - "versionMajor": 8, - "versionMinor": 0, - "process": [], - "hmMonitorInterface": [ - { - "instanceSpecifier": "demo/demo_application""" - + str(index) - + """/Port1", - "processShortName": "demo_application""" - + str(index) - + """", - "portPrototype": "Port1", - "interfacePath": "demo_application_""" - + str(index) - + "_" - + process_group - + """", - "refProcessIndex":0 - } - ] -} -""" - ) - return config - - -if __name__ == "__main__": - my_parser = argparse.ArgumentParser() - my_parser.add_argument( - "-c", - "--cppprocesses", - action="store", - type=int, - required=True, - help="Number of C++ processes", - ) - my_parser.add_argument( - "-r", - "--rustprocesses", - action="store", - type=int, - required=True, - help="Number of Rust processes", - ) - my_parser.add_argument( - "-p", - "--process_groups", - nargs="+", - help="Name of a Process Group", - required=True, - ) - my_parser.add_argument( - "-o", "--out", action="store", type=Path, required=True, help="Output directory" - ) - args = my_parser.parse_args() - - process_count = args.cppprocesses + args.rustprocesses - for process_group_index in range(0, len(args.process_groups)): - process_group = args.process_groups[process_group_index] - for process_index in get_process_index_range( - process_count, process_group_index - ): - cfg_out_path = os.path.join( - args.out, - f"health_monitor_process_cfg_{process_index}_{process_group}.json", - ) - with open(cfg_out_path, "w") as f: - f.write(gen_health_monitor_process_cfg(process_index, process_group)) diff --git a/examples/config/gen_launch_manager_cfg.py b/examples/config/gen_launch_manager_cfg.py deleted file mode 100644 index ea1dc511..00000000 --- a/examples/config/gen_launch_manager_cfg.py +++ /dev/null @@ -1,530 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -import argparse -import json -from pathlib import Path -import os -from gen_common_cfg import get_process_index_range - - -class LaunchManagerConfGen: - def __init__(self): - # setup generator data structures - self.machines = [] - - def generate_json(self, out_path): - # generate all configured machines - for machine in self.machines: - json_config = { - "versionMajor": 7, - "versionMinor": 0, - } - - json_config["Process"] = [] - json_config["ModeGroup"] = [] - for process_group in machine["process_groups"].keys(): - # configuring processes - for process in machine["process_groups"][process_group][ - "processes" - ].keys(): - json_config["Process"].append( - { - "identifier": f"{process}", - "uid": machine["process_groups"][process_group][ - "processes" - ][process]["uid"], - "gid": machine["process_groups"][process_group][ - "processes" - ][process]["gid"], - "path": machine["process_groups"][process_group][ - "processes" - ][process]["executable_name"], - } - ) - - if ( - machine["process_groups"][process_group]["processes"][process][ - "special_rights" - ] - != "" - ): - json_config["Process"][-1]["functionClusterAffiliation"] = ( - machine["process_groups"][process_group]["processes"][ - process - ]["special_rights"] - ) - - json_config["Process"][-1]["numberOfRestartAttempts"] = machine[ - "process_groups" - ][process_group]["processes"][process]["restart_attempts"] - - if not machine["process_groups"][process_group]["processes"][ - process - ]["native_application"]: - json_config["Process"][-1]["executable_reportingBehavior"] = ( - "ReportsExecutionState" - ) - else: - json_config["Process"][-1]["executable_reportingBehavior"] = ( - "DoesNotReportExecutionState" - ) - - json_config["Process"][-1]["sgids"] = [] - for gid in machine["process_groups"][process_group]["processes"][ - process - ]["supplementary_group_ids"]: - json_config["Process"][-1]["sgids"].append({"sgid": gid}) - - json_config["Process"][-1]["startupConfig"] = [] - for startup_config in machine["process_groups"][process_group][ - "processes" - ][process]["startup_configs"].keys(): - config = machine["process_groups"][process_group]["processes"][ - process - ]["startup_configs"][startup_config] - json_config["Process"][-1]["startupConfig"].append( - { - "executionError": f"{config['execution_error']}", - "schedulingPolicy": config["scheduling_policy"], - "schedulingPriority": f"{config['scheduling_priority']}", - "identifier": startup_config, - "enterTimeoutValue": int( - config["enter_timeout"] * 1000 - ), # convert to ms - "exitTimeoutValue": int( - config["exit_timeout"] * 1000 - ), # convert to ms - "terminationBehavior": config["termination_behavior"], - "executionDependency": [], - "processGroupStateDependency": [], - } - ) - - json_config["Process"][-1]["startupConfig"][-1][ - "executionDependency" - ] = [] - for process, state in config["depends_on"].items(): - json_config["Process"][-1]["startupConfig"][-1][ - "executionDependency" - ].append( - { - "stateName": state, - "targetProcess_identifier": f"/{process}App/{process}", - } - ) - - for state in config["use_in"]: - json_config["Process"][-1]["startupConfig"][-1][ - "processGroupStateDependency" - ].append( - { - "stateMachine_name": f"{process_group}", - "stateName": f"{process_group}/{state}", - } - ) - - json_config["Process"][-1]["startupConfig"][-1][ - "environmentVariable" - ] = [] - for key, val in config["env_variables"].items(): - json_config["Process"][-1]["startupConfig"][-1][ - "environmentVariable" - ].append({"key": key, "value": val}) - - json_config["Process"][-1]["startupConfig"][-1][ - "processArgument" - ] = [] - for arg in config["process_arguments"]: - json_config["Process"][-1]["startupConfig"][-1][ - "processArgument" - ].append({"argument": arg}) - - # configuring process groups - json_config["ModeGroup"].append( - { - "identifier": f"{process_group}", - "initialMode_name": "Off", - "recoveryMode_name": f"{process_group}/Recovery", - "modeDeclaration": [], - } - ) - for state in machine["process_group_states"][ - machine["process_groups"][process_group][ - "process_group_states_name" - ] - ]: - # replicating bug where we mix ModeDeclarationGroups (Process Group States) and ProcessGroupSet (Process Groups) - # essentially we use Process Group States declaration as Process Groups declarations - # here we should use machine["process_groups"][process_group]["process_group_states_name"] instead of process_group - # but we need to create new process group states declaration on the fly, so each process group has a unique set of states - json_config["ModeGroup"][-1]["modeDeclaration"].append( - {"identifier": f"{process_group}/{state}"} - ) - - file = open( - out_path, - "w", - ) - file.write(json.dumps(json_config, indent=2)) - file.close() - - def add_machine( - self, - name, - default_application_timeout_enter=0.5, - default_application_timeout_exit=0.5, - env_variables={"LD_LIBRARY_PATH": "/opt/lib"}, - ): - # TODO: this is only a test code, so for various reasons we only support a single machine configuration - if len(self.machines) > 0: - raise Exception( - "This version of ConfGen only support configuration of a single machine!" - ) - - for machine in self.machines: - if name == machine["machine_name"]: - raise Exception(f"Machine with {name=} cannot be redefined!") - - self.machines.append( - { - "machine_name": name, - "default_application_timeout_enter": default_application_timeout_enter, - "default_application_timeout_exit": default_application_timeout_exit, - "env_variables": env_variables, - # by default machine doesn't have any process groups - "process_groups": {}, - "process_group_states": {}, - } - ) - - # returning the freshly created machine, so it can be extended elsewhere - # machine index is also included, as it could be used later to read machine wide default values - index = len(self.machines) - 1 - return {"machine": self.machines[index], "machine_index": index} - - def machine_add_process_group(self, machine, name, states=["Off", "Verify"]): - pg_states_index = "" - - # process group states should be reused among different process groups - for key, value in machine["machine"]["process_group_states"].items(): - if value == states: - pg_states_index = key - - if "" == pg_states_index: - # TODO: at the moment this code generator only support a single machine, - # so we don't need to think about name space clashes between different machines... - # code like this should prevent this: - # pg_states_index = f"{machine['machine_name']}_{name}_States" - - # those process group states were not defined before - pg_states_index = name - machine["machine"]["process_group_states"][pg_states_index] = states - - if name not in machine["machine"]["process_groups"].keys(): - machine["machine"]["process_groups"][name] = { - "process_group_states_name": pg_states_index, - "processes": {}, - } - else: - raise Exception(f"Process Group with {name=} cannot be redefined!") - - # returning the freshly created process_group, so it can be extended elsewhere - # machine index is also included, as it could be used later to read machine wide default values - return { - "process_group": machine["machine"]["process_groups"][name], - "machine_index": machine["machine_index"], - } - - def process_group_add_process( - self, - process_group, - name, - executable_name=None, - uid=1001, - gid=1001, - supplementary_group_ids=[], - restart_attempts=0, - native_application=False, - special_rights="", - ): - if executable_name is None: - executable_name = f"/opt/apps/{name}/{name}" - - if name not in process_group["process_group"]["processes"].keys(): - process_group["process_group"]["processes"][name] = { - "executable_name": executable_name, - "uid": uid, - "gid": gid, - "supplementary_group_ids": supplementary_group_ids, - "restart_attempts": restart_attempts, - "native_application": native_application, - "special_rights": special_rights, - # by default process doesn't have any startup configs - "startup_configs": {}, - } - else: - raise Exception(f"Process with {name=} cannot be redefined!") - - # returning process config, so user can add startup configs - # machine index is also included, as it could be used later to read machine wide default values - return { - "process": process_group["process_group"]["processes"][name], - "machine_index": process_group["machine_index"], - } - - def process_add_startup_config( - self, - process, - name, - process_arguments=[], - env_variables={}, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=None, - exit_timeout=None, - execution_error=1, - depends_on={}, - use_in=[], - termination_behavior="ProcessIsNotSelfTerminating", - ): - if enter_timeout is None: - enter_timeout = self.machines[process["machine_index"]][ - "default_application_timeout_enter" - ] - - if exit_timeout is None: - exit_timeout = self.machines[process["machine_index"]][ - "default_application_timeout_exit" - ] - - # merging machine wide env variables with startup config (aka local) env variables - # step 1 --> start from empty set - merged_env_variables = {} - # step 2 --> overtake all env variables from global configuration - for key, val in self.machines[process["machine_index"]][ - "env_variables" - ].items(): - merged_env_variables[key] = val - # step 3 --> overtake all env variables from startup config - # please note that this step has to happen last, as local configuration should override global configuration - # to fulfill our requirements - for key, val in env_variables.items(): - merged_env_variables[key] = val - - if name not in process["process"]["startup_configs"].keys(): - process["process"]["startup_configs"][name] = { - "process_arguments": process_arguments, - "env_variables": merged_env_variables, - "scheduling_policy": scheduling_policy, - "scheduling_priority": scheduling_priority, - "enter_timeout": enter_timeout, - "exit_timeout": exit_timeout, - "execution_error": execution_error, - "depends_on": depends_on, - "use_in": use_in, - "termination_behavior": termination_behavior, - } - else: - raise Exception(f"Startup configuration with {name=} cannot be redefined!") - - # no need to return anything - # end of the configuration - - -def is_rust_app(process_index: int, cppprocess_count: int, rustprocess_count: int): - processes_per_process_group = cppprocess_count + rustprocess_count - process_index = process_index % processes_per_process_group - return process_index >= cppprocess_count - - -if __name__ == "__main__": - my_parser = argparse.ArgumentParser() - my_parser.add_argument( - "-c", - "--cppprocesses", - action="store", - type=int, - required=True, - help="Number of C++ demo app processes", - ) - my_parser.add_argument( - "-r", - "--rustprocesses", - action="store", - type=int, - required=True, - help="Number of Rust processes", - ) - my_parser.add_argument( - "-p", - "--process_groups", - nargs="+", - help="Name of a Process Group", - required=True, - ) - my_parser.add_argument( - "-n", - "--non-supervised-processes", - action="store", - type=int, - required=True, - help="Number of C++ non supervised demo app processes (no health manager involved)", - ) - my_parser.add_argument( - "-o", "--out", action="store", type=Path, required=True, help="Output directory" - ) - args = my_parser.parse_args() - - conf_gen = LaunchManagerConfGen() - qt_am_machine = conf_gen.add_machine( - "qt_am_machine", env_variables={"LD_LIBRARY_PATH": "/opt/lib"} - ) - - BASE_PROCESS_GROUP = "MainPG" - process_groups = args.process_groups - if BASE_PROCESS_GROUP not in process_groups: - print( - f"Process group '{BASE_PROCESS_GROUP}' must be included in the process groups list" - ) - exit(1) - - # adding function groups to TestMachine01 - pg_machine = conf_gen.machine_add_process_group( - qt_am_machine, BASE_PROCESS_GROUP, ["Off", "Startup", "Recovery"] - ) - - # adding Control application - control_process = conf_gen.process_group_add_process( - pg_machine, - "control_daemon", - executable_name="/opt/control_app/control_daemon", - uid=0, - gid=0, - special_rights="STATE_MANAGEMENT", - ) - conf_gen.process_add_startup_config( - control_process, - "control_daemon_startup_config", - # process_arguments = ["-a", "-b", "--test"], - env_variables={ - "PROCESSIDENTIFIER": "control_daemon", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=1.0, - exit_timeout=1.0, - use_in=["Startup", "Recovery"], - ) - - if ( - args.cppprocesses < 0 - or args.non_supervised_processes < 0 - or args.non_supervised_processes > 10000 - or args.cppprocesses > 10000 - ): - print("Number of demo app processes must be between 0 and 1000") - exit(1) - if args.rustprocesses < 0 or args.rustprocesses > 10000: - print("Number of demo app processes must be between 0 and 1000") - exit(1) - total_process_count = args.cppprocesses + args.rustprocesses - - for process_group_index in range(0, len(process_groups)): - process_group_name = process_groups[process_group_index] - if process_group_name == BASE_PROCESS_GROUP: - pg = pg_machine - exec_dependency = {"healthmonitor": "Running"} - else: - pg = conf_gen.machine_add_process_group( - qt_am_machine, process_group_name, ["Off", "Startup", "Recovery"] - ) - exec_dependency = {} - - for i in get_process_index_range(total_process_count, process_group_index): - if not is_rust_app(i, args.cppprocesses, args.rustprocesses): - demo_executable_path = "/opt/supervision_demo/cpp_supervised_app" - print( - f"CPP Process with index {i} in process group {process_group_index}" - ) - else: - demo_executable_path = "/opt/supervision_demo/rust_supervised_app" - print( - f"Rust Process with index {i} in process group {process_group_index}" - ) - - demo_process = conf_gen.process_group_add_process( - pg, - f"demo_app{i}_{process_group_name}", - executable_name=demo_executable_path, - uid=0, - gid=0, - ) - conf_gen.process_add_startup_config( - demo_process, - f"demo_app_startup_config_{i}", - process_arguments=["-d50"], - env_variables={ - "PROCESSIDENTIFIER": f"{process_group_name}_app{i}", - "CONFIG_PATH": f"/opt/supervision_demo/etc/health_monitor_process_cfg_{i}_{process_group_name}.bin", - "IDENTIFIER": f"demo/demo_application{i}/Port1", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=2.0, - exit_timeout=2.0, - depends_on=exec_dependency, - use_in=["Startup"], - ) - - for i in range(args.non_supervised_processes): - demo_process_wo_hm = conf_gen.process_group_add_process( - pg, - f"{process_group_name}_lifecycle_app{i}", - executable_name="/opt/cpp_lifecycle_app/cpp_lifecycle_app", - uid=0, - gid=0, - ) - conf_gen.process_add_startup_config( - demo_process_wo_hm, - f"lifecycle_app_startup_config_{i}_{process_group_name}_", - # uncomment one of the two following lines to inject error - # process_arguments=["-c", "2000"] if i == 1 else [], - # process_arguments=["-s"] if i == 1 else [], - env_variables={ - "PROCESSIDENTIFIER": f"{process_group_name}_lc{i}", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=2.0, - exit_timeout=2.0, - use_in=["Startup"], - ) - - # One of the processes should also run in recovery state with different configuration - if i == args.non_supervised_processes - 1: - conf_gen.process_add_startup_config( - demo_process_wo_hm, - f"lifecycle_app_startup_config_{i}_{process_group_name}_recovery", - process_arguments=[f"-v"], - env_variables={ - "PROCESSIDENTIFIER": f"{process_group_name}_lc{i}", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=2.0, - exit_timeout=2.0, - use_in=["Recovery"], - ) - - cfg_out_path = os.path.join(args.out, "lm_demo.json") - conf_gen.generate_json(cfg_out_path) diff --git a/examples/config/hmcore.json b/examples/config/hmcore.json deleted file mode 100644 index d769a798..00000000 --- a/examples/config/hmcore.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "versionMajor": 3, - "versionMinor": 0, - "watchdogs": [], - "config": [ - { - "periodicity": 50, - "bufferSizeGlobalSupervision": "512" - } - ] -} \ No newline at end of file From 71a7b3127ccb80b3e5052b7334c746444a55dd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 10 Mar 2026 15:23:15 +0100 Subject: [PATCH 67/72] Fix formatting --- defs.bzl | 2 +- examples/BUILD | 26 +++++---- examples/config/BUILD | 5 +- examples/run_examples.bzl | 2 +- scripts/config_mapping/BUILD | 17 +++--- scripts/config_mapping/config.bzl | 38 ++++++------- scripts/config_mapping/integration_tests.py | 36 +++++++++---- scripts/config_mapping/lifecycle_config.py | 54 ++++++++++++++----- scripts/config_mapping/unit_tests.py | 1 + .../config/config_schema/BUILD | 2 +- tests/integration/BUILD | 3 +- tests/integration/smoke/BUILD | 15 +++--- 12 files changed, 127 insertions(+), 74 deletions(-) diff --git a/defs.bzl b/defs.bzl index 278a2ca0..76ab3cc6 100644 --- a/defs.bzl +++ b/defs.bzl @@ -16,4 +16,4 @@ # --- Launch Manager Configuration --- load("//scripts/config_mapping:config.bzl", _launch_manager_config = "launch_manager_config") -launch_manager_config = _launch_manager_config \ No newline at end of file +launch_manager_config = _launch_manager_config diff --git a/examples/BUILD b/examples/BUILD index 230b9906..f8453faf 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -2,32 +2,36 @@ load("//:defs.bzl", "launch_manager_config") load(":run_examples.bzl", "run_examples") launch_manager_config( - name ="example_config", - config="//examples/config:lifecycle_demo_config") + name = "example_config", + config = "//examples/config:lifecycle_demo_config", +) filegroup( name = "example_apps", srcs = [ + "//examples:example_config", "//examples/control_application:control_daemon", "//examples/control_application:lmcontrol", - "//examples/cpp_lifecycle_app:cpp_lifecycle_app", - "//examples/cpp_supervised_app:cpp_supervised_app", - "//examples/rust_supervised_app:rust_supervised_app", - "//examples:example_config" + "//examples/cpp_lifecycle_app", + "//examples/cpp_supervised_app", + "//examples/rust_supervised_app", ], ) filegroup( name = "lm_binaries", srcs = [ + "//src/control_client_lib", "//src/launch_manager_daemon:launch_manager", - "//src/launch_manager_daemon/process_state_client_lib:process_state_client", "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", - "//src/control_client_lib:control_client_lib", - ] + "//src/launch_manager_daemon/process_state_client_lib:process_state_client", + ], ) run_examples( name = "run_examples", - deps = [":example_apps", ":lm_binaries"], -) \ No newline at end of file + deps = [ + ":example_apps", + ":lm_binaries", + ], +) diff --git a/examples/config/BUILD b/examples/config/BUILD index b786c706..1e0ce8dd 100644 --- a/examples/config/BUILD +++ b/examples/config/BUILD @@ -1,6 +1,7 @@ exports_files( - ["lifecycle_demo.json"], - visibility = ["//examples:__subpackages__"]) + ["lifecycle_demo.json"], + visibility = ["//examples:__subpackages__"], +) filegroup( name = "lifecycle_demo_config", diff --git a/examples/run_examples.bzl b/examples/run_examples.bzl index 5eed488b..3c3914a3 100644 --- a/examples/run_examples.bzl +++ b/examples/run_examples.bzl @@ -31,4 +31,4 @@ run_examples = rule( allow_single_file = True, ), }, -) \ No newline at end of file +) diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD index ac021217..074d0823 100644 --- a/scripts/config_mapping/BUILD +++ b/scripts/config_mapping/BUILD @@ -1,33 +1,34 @@ - load("@score_lifecycle_pip//:requirements.bzl", "requirement") load("@score_tooling//:defs.bzl", "score_py_pytest") py_binary( name = "lifecycle_config", srcs = ["lifecycle_config.py"], - deps = [requirement("jsonschema")], visibility = ["//visibility:public"], + deps = [requirement("jsonschema")], ) filegroup( - name = "integration_test_files", - srcs = glob(["tests/**/*/*.json"]), + name = "integration_test_files", + srcs = glob(["tests/**/*/*.json"]), ) score_py_pytest( name = "lifecycle_config_tests", srcs = [ - "unit_tests.py", "integration_tests.py", + "unit_tests.py", ], args = [ ], data = [ + ":integration_test_files", ":lifecycle_config.py", "//src/launch_manager_daemon/config/config_schema:s-core_launch_manager.schema.json", - ":integration_test_files" ], tags = ["manual"], - deps = [requirement("pytest"), requirement("jsonschema")], - + deps = [ + requirement("pytest"), + requirement("jsonschema"), + ], ) diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl index 14a97f00..21a5a34c 100644 --- a/scripts/config_mapping/config.bzl +++ b/scripts/config_mapping/config.bzl @@ -16,9 +16,11 @@ def _launch_manager_config_impl(ctx): progress_message = "generating Launch Manager config from {}".format(config.short_path), arguments = [ config.path, - "--schema", schema.path, - "-o", gen_dir_json.path - ] + "--schema", + schema.path, + "-o", + gen_dir_json.path, + ], ) flatbuffer_out_dir = ctx.attr.flatbuffer_out_dir @@ -26,7 +28,7 @@ def _launch_manager_config_impl(ctx): lm_schema = ctx.file.lm_schema hm_schema = ctx.file.hm_schema hmcore_schema = ctx.file.hmcore_schema - + # We compile each of them via flatbuffer. # Based on the name of each generated file, we select the corresponding schema. gen_dir_flatbuffer = ctx.actions.declare_directory(flatbuffer_out_dir) @@ -59,28 +61,27 @@ def _launch_manager_config_impl(ctx): fi done """.format( - gen_dir_flatbuffer=gen_dir_flatbuffer.path, - gen_dir_json=gen_dir_json.path, - lm_schema=lm_schema.path, - hmcore_schema=hmcore_schema.path, - hm_schema=hm_schema.path, - flatc=flatc.path + gen_dir_flatbuffer = gen_dir_flatbuffer.path, + gen_dir_json = gen_dir_json.path, + lm_schema = lm_schema.path, + hmcore_schema = hmcore_schema.path, + hm_schema = hm_schema.path, + flatc = flatc.path, ), arguments = [], mnemonic = "LaunchManagerFlatbufferConfigGeneration", - progress_message = "compiling generated Launch Manager configs in {} to flatbuffer files in {}".format(gen_dir_json.short_path, gen_dir_flatbuffer.short_path) + progress_message = "compiling generated Launch Manager configs in {} to flatbuffer files in {}".format(gen_dir_json.short_path, gen_dir_flatbuffer.short_path), ) rf = ctx.runfiles( files = [gen_dir_flatbuffer], root_symlinks = { ("_main/" + ctx.attr.flatbuffer_out_dir): gen_dir_flatbuffer, - } + }, ) return DefaultInfo(files = depset([gen_dir_flatbuffer]), runfiles = rf) - launch_manager_config = rule( implementation = _launch_manager_config_impl, attrs = { @@ -90,7 +91,7 @@ launch_manager_config = rule( doc = "Json file to convert. Note that the binary file will have the same name as the json (minus the suffix)", ), "schema": attr.label( - default=Label("//src/launch_manager_daemon/config/config_schema:s-core_launch_manager.schema.json"), + default = Label("//src/launch_manager_daemon/config/config_schema:s-core_launch_manager.schema.json"), allow_single_file = [".json"], doc = "Json schema file to validate the input json against", ), @@ -121,14 +122,13 @@ launch_manager_config = rule( ), "hm_schema": attr.label( allow_single_file = [".fbs"], - default=Label("//src/launch_manager_daemon/health_monitor_lib:hm_flatcfg_fbs"), + default = Label("//src/launch_manager_daemon/health_monitor_lib:hm_flatcfg_fbs"), doc = "HealthMonitor fbs file to use", ), "hmcore_schema": attr.label( allow_single_file = [".fbs"], - default=Label("//src/launch_manager_daemon/health_monitor_lib:hmcore_flatcfg_fbs"), + default = Label("//src/launch_manager_daemon/health_monitor_lib:hmcore_flatcfg_fbs"), doc = "HealthMonitor core fbs file to use", - ) - } + ), + }, ) - diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 0f0c3da9..56626e94 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -12,7 +12,14 @@ ) script_dir = Path(__file__).parent -schema_path = script_dir.parent.parent / "src" / "launch_manager_daemon" / "config" / "config_schema" / "s-core_launch_manager.schema.json" +schema_path = ( + script_dir.parent.parent + / "src" + / "launch_manager_daemon" + / "config" + / "config_schema" + / "s-core_launch_manager.schema.json" +) tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" @@ -161,6 +168,7 @@ def test_empty_launch_config_mapping(): run(input_file, test_name, compare_files_only=["lm_demo.json"]) + def test_custom_validation_failures(): """ Test that custom validation checks implemented in lifecycle_config.py are correctly identifying invalid configurations. @@ -176,23 +184,29 @@ def test_custom_validation_failures(): try: run(input_file, test_name) - raise AssertionError("Expected an error due to custom validation failures, but the mapping script executed successfully.") + raise AssertionError( + "Expected an error due to custom validation failures, but the mapping script executed successfully." + ) except subprocess.CalledProcessError as e: - assert e.returncode == CUSTOM_VALIDATION_FAILURE, f"Expected exit code {CUSTOM_VALIDATION_FAILURE}, got {e.returncode}" + assert e.returncode == CUSTOM_VALIDATION_FAILURE, ( + f"Expected exit code {CUSTOM_VALIDATION_FAILURE}, got {e.returncode}" + ) expected_errors = [ - "recovery RunTarget must be set to \"fallback_run_target\"", + 'recovery RunTarget must be set to "fallback_run_target"', "fallback_run_target is a mandatory configuration", - "RunTarget name \"fallback_run_target\" is reserved", + 'RunTarget name "fallback_run_target" is reserved', "initial_run_target must be configured to 'Startup'", - "\"Startup\" is a mandatory RunTarget" + '"Startup" is a mandatory RunTarget', ] actual_error_output = e.stderr for expected_error in expected_errors: if expected_error not in actual_error_output: print(f"Expected error message not found: {expected_error}") print(f"Actual error output: {actual_error_output}") - raise AssertionError(f"Expected error message not found: {expected_error}") + raise AssertionError( + f"Expected error message not found: {expected_error}" + ) def test_schema_validation_failures(): @@ -206,6 +220,10 @@ def test_schema_validation_failures(): try: run(input_file, test_name) - raise AssertionError("Expected an error due to schema validation failures, but the mapping script executed successfully.") + raise AssertionError( + "Expected an error due to schema validation failures, but the mapping script executed successfully." + ) except subprocess.CalledProcessError as e: - assert e.returncode == SCHEMA_VALIDATION_FAILURE, f"Expected exit code {SCHEMA_VALIDATION_FAILURE}, got {e.returncode}" + assert e.returncode == SCHEMA_VALIDATION_FAILURE, ( + f"Expected exit code {SCHEMA_VALIDATION_FAILURE}, got {e.returncode}" + ) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index d8645f41..71f2e82a 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -58,9 +58,11 @@ } """) + def report_error(message): print(f"Error: {message}", file=sys.stderr) + # There are various dictionaries in the config where only a single entry is allowed. # We do not want to merge the defaults with the user specified values for these dictionaries. not_merging_dicts = ["ready_recovery_action", "recovery_action"] @@ -76,6 +78,7 @@ def get_recovery_process_group_state(config): # Existence has already been validated in the custom_validations function return "MainPG/fallback_run_target" + def sec_to_ms(sec: float) -> int: return int(sec * 1000) @@ -296,7 +299,9 @@ def get_all_refProcessGroupStates(run_targets): ] hm_config["hmLocalSupervision"].append(local_supervision) - with open(f"{output_dir}/hmproc_{component_name}.json", "w") as process_file: + with open( + f"{output_dir}/hmproc_{component_name}.json", "w" + ) as process_file: process_config = {} process_config["versionMajor"] = HM_SCHEMA_VERSION_MAJOR process_config["versionMinor"] = HM_SCHEMA_VERSION_MINOR @@ -351,7 +356,7 @@ def get_all_refProcessGroupStates(run_targets): ) } ] - + if watchdog_config := config.get("watchdog", {}): watchdog = {} watchdog["shortName"] = "watchdog" @@ -600,6 +605,7 @@ def get_terminating_behavior(component_config): with open(f"{output_dir}/lm_demo.json", "w") as lm_file: json.dump(lm_config, lm_file, indent=4) + def custom_validations(config): success = True @@ -611,26 +617,33 @@ def custom_validations(config): if "Startup" not in config["run_targets"]: report_error( - "\"Startup\" is a mandatory RunTarget and must be defined in the configuration." + '"Startup" is a mandatory RunTarget and must be defined in the configuration.' ) success = False - if "fallback_run_target" in config["run_targets"]: report_error( - "RunTarget name \"fallback_run_target\" is reserved, please choose a different name." + 'RunTarget name "fallback_run_target" is reserved, please choose a different name.' ) success = False # Check that for any switch_run_target recovery action, the run_target is set to "fallback_run_target" for _, run_target in config["run_targets"].items(): - recovery_target_name = run_target.get("recovery_action", {}).get("switch_run_target", {}).get("run_target", "fallback_run_target") + recovery_target_name = ( + run_target.get("recovery_action", {}) + .get("switch_run_target", {}) + .get("run_target", "fallback_run_target") + ) if recovery_target_name != "fallback_run_target": - report_error("For any switch_run_target recovery action, the recovery RunTarget must be set to \"fallback_run_target\".") + report_error( + 'For any switch_run_target recovery action, the recovery RunTarget must be set to "fallback_run_target".' + ) success = False if "fallback_run_target" not in config: - report_error("fallback_run_target is a mandatory configuration but was not found in the config.") + report_error( + "fallback_run_target is a mandatory configuration but was not found in the config." + ) success = False return success @@ -640,26 +653,35 @@ def check_validation_dependency(): try: import jsonschema except ImportError: - print("jsonschema library is not installed. Please install it with \"pip install jsonschema\" to enable schema validation.") + print( + 'jsonschema library is not installed. Please install it with "pip install jsonschema" to enable schema validation.' + ) return False return True + def schema_validation(json_input, schema): try: from jsonschema import validate, ValidationError + validate(json_input, schema) print("Schema Validation successful") return True except ValidationError as err: - print(f"Error: Schema validation failed with error: {err.message}", file=sys.stderr) + print( + f"Error: Schema validation failed with error: {err.message}", + file=sys.stderr, + ) return False + # Possible exit codes returned from this script SUCCESS = 0 SCHEMA_VALIDATION_DEPENDENCY_ERROR = 1 SCHEMA_VALIDATION_FAILURE = 2 CUSTOM_VALIDATION_FAILURE = 3 + def main(): parser = argparse.ArgumentParser() parser.add_argument("filename", help="The launch_manager configuration file") @@ -676,7 +698,7 @@ def main(): args = parser.parse_args() input_config = load_json_file(args.filename) - + if args.schema: # User asked for validation explicitly, but the dependency is not installed, we should exit with an error if args.validate and not check_validation_dependency(): @@ -684,18 +706,22 @@ def main(): # User asked not explicitly for validation, but the dependency is not installed, we should print a warning and continue without validation if not check_validation_dependency(): - print("lifecycle_config.py:jsonschema library is not installed. Please install it with \"pip install jsonschema\" to enable schema validation.") + print( + 'lifecycle_config.py:jsonschema library is not installed. Please install it with "pip install jsonschema" to enable schema validation.' + ) print("Schema validation will be skipped.") else: json_schema = load_json_file(args.schema) validation_successful = schema_validation(input_config, json_schema) if not validation_successful: exit(SCHEMA_VALIDATION_FAILURE) - + if args.validate: exit(SUCCESS) else: - print("No schema provided, skipping validation. Provide the path to the json schema with \"--schema \" to enable validation.") + print( + 'No schema provided, skipping validation. Provide the path to the json schema with "--schema " to enable validation.' + ) preprocessed_config = preprocess_defaults(score_defaults, input_config) if not custom_validations(preprocessed_config): diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py index 1c993daf..33635d2e 100644 --- a/scripts/config_mapping/unit_tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -2,6 +2,7 @@ from scripts.config_mapping.lifecycle_config import preprocess_defaults import json + def test_preprocessing_basic(): """ Basic smoketest for the preprocess_defaults function, to ensure that defaults are being applied and overridden correctly. diff --git a/src/launch_manager_daemon/config/config_schema/BUILD b/src/launch_manager_daemon/config/config_schema/BUILD index 4a754d37..913cf229 100644 --- a/src/launch_manager_daemon/config/config_schema/BUILD +++ b/src/launch_manager_daemon/config/config_schema/BUILD @@ -1 +1 @@ -exports_files(["s-core_launch_manager.schema.json"]) \ No newline at end of file +exports_files(["s-core_launch_manager.schema.json"]) diff --git a/tests/integration/BUILD b/tests/integration/BUILD index 7c4b0659..4fa07631 100644 --- a/tests/integration/BUILD +++ b/tests/integration/BUILD @@ -1,3 +1,5 @@ +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + # ******************************************************************************* # Copyright (c) 2026 Contributors to the Eclipse Foundation # @@ -11,7 +13,6 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* load("@score_lifecycle_pip//:requirements.bzl", "all_requirements") -load("@rules_python//python:pip.bzl", "compile_pip_requirements") load("@score_tooling//python_basics:defs.bzl", "score_py_pytest", "score_virtualenv") # In order to update the requirements, change the `requirements.txt` file and run: diff --git a/tests/integration/smoke/BUILD b/tests/integration/smoke/BUILD index 01457b5d..2727804a 100644 --- a/tests/integration/smoke/BUILD +++ b/tests/integration/smoke/BUILD @@ -12,17 +12,18 @@ # ******************************************************************************* load("@score_lifecycle_pip//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") +load("//:defs.bzl", "launch_manager_config") load("//config:flatbuffers_rules.bzl", "flatbuffer_json_to_bin") exports_files( - ["lm_demo.json"], - visibility = ["//examples:__subpackages__"]) + ["lm_demo.json"], + visibility = ["//examples:__subpackages__"], +) -load("//:defs.bzl", "launch_manager_config") launch_manager_config( - name ="lm_smoketest_config", - config="//tests/integration/smoke:lifecycle_smoketest.json", - flatbuffer_out_dir="etc" + name = "lm_smoketest_config", + config = "//tests/integration/smoke:lifecycle_smoketest.json", + flatbuffer_out_dir = "etc", ) cc_binary( @@ -58,8 +59,8 @@ score_py_pytest( data = [ ":control_daemon_mock", ":gtest_process", + ":lm_smoketest_config", "//src/launch_manager_daemon:launch_manager", - ":lm_smoketest_config" ], tags = [ "integration", From 370afa6852f6d68ae9cc3c1ae8ba4bbe44461e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 10 Mar 2026 15:24:08 +0100 Subject: [PATCH 68/72] Fix copyright headers --- examples/BUILD | 12 ++++++++++++ examples/config/BUILD | 12 ++++++++++++ examples/run_examples.bzl | 12 ++++++++++++ src/launch_manager_daemon/config/BUILD | 12 ++++++++++++ src/launch_manager_daemon/config/config_schema/BUILD | 12 ++++++++++++ 5 files changed, 60 insertions(+) diff --git a/examples/BUILD b/examples/BUILD index f8453faf..5b76c1a1 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -1,3 +1,15 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* load("//:defs.bzl", "launch_manager_config") load(":run_examples.bzl", "run_examples") diff --git a/examples/config/BUILD b/examples/config/BUILD index 1e0ce8dd..16a9aefc 100644 --- a/examples/config/BUILD +++ b/examples/config/BUILD @@ -1,3 +1,15 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* exports_files( ["lifecycle_demo.json"], visibility = ["//examples:__subpackages__"], diff --git a/examples/run_examples.bzl b/examples/run_examples.bzl index 3c3914a3..05a5f8c6 100644 --- a/examples/run_examples.bzl +++ b/examples/run_examples.bzl @@ -1,3 +1,15 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* def _impl_run_examples(ctx): run_script = ctx.file._run_script diff --git a/src/launch_manager_daemon/config/BUILD b/src/launch_manager_daemon/config/BUILD index e4b6d642..67f38aa8 100644 --- a/src/launch_manager_daemon/config/BUILD +++ b/src/launch_manager_daemon/config/BUILD @@ -1,3 +1,15 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* exports_files([ "lm_flatcfg.fbs", "lm_flatcfg_generated.h", diff --git a/src/launch_manager_daemon/config/config_schema/BUILD b/src/launch_manager_daemon/config/config_schema/BUILD index 913cf229..6b2338ca 100644 --- a/src/launch_manager_daemon/config/config_schema/BUILD +++ b/src/launch_manager_daemon/config/config_schema/BUILD @@ -1 +1,13 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* exports_files(["s-core_launch_manager.schema.json"]) From c1f4391b225d5b3e7ba76ca43b647c64302d0d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Tue, 10 Mar 2026 15:32:53 +0100 Subject: [PATCH 69/72] Tidy MODULE.bazel file --- MODULE.bazel | 1 - 1 file changed, 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index b1fba7ae..9ccb31e6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -116,7 +116,6 @@ python.toolchain( is_default = True, python_version = PYTHON_VERSION, ) -use_repo(python) # Python pip dependencies pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") From e538003a2cdb30b03c78158894dbd7289ea99479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 11 Mar 2026 08:52:59 +0100 Subject: [PATCH 70/72] Improve schema validation error message --- scripts/config_mapping/lifecycle_config.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 71f2e82a..b6b9db77 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -660,7 +660,7 @@ def check_validation_dependency(): return True -def schema_validation(json_input, schema): +def schema_validation(json_input, schema, config_path=None, schema_path=None): try: from jsonschema import validate, ValidationError @@ -668,8 +668,12 @@ def schema_validation(json_input, schema): print("Schema Validation successful") return True except ValidationError as err: + path = " -> ".join(str(p) for p in err.absolute_path) + location = f" (at: {path})" if path else "" + config_info = f"Validated Config: {config_path}" if config_path else "" + schema_info = f"Schema File: {schema_path}" if schema_path else "" print( - f"Error: Schema validation failed with error: {err.message}", + f"Error: Schema validation failed{location}: {err.message}\n{config_info}\n{schema_info}", file=sys.stderr, ) return False @@ -712,7 +716,7 @@ def main(): print("Schema validation will be skipped.") else: json_schema = load_json_file(args.schema) - validation_successful = schema_validation(input_config, json_schema) + validation_successful = schema_validation(input_config, json_schema, config_path=args.filename, schema_path=args.schema) if not validation_successful: exit(SCHEMA_VALIDATION_FAILURE) From 7882e2b6c55beb1fa99ee8ebf766c1d1c907b4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 11 Mar 2026 11:16:18 +0100 Subject: [PATCH 71/72] Fix formatting --- scripts/config_mapping/lifecycle_config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index b6b9db77..8812678f 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -716,7 +716,12 @@ def main(): print("Schema validation will be skipped.") else: json_schema = load_json_file(args.schema) - validation_successful = schema_validation(input_config, json_schema, config_path=args.filename, schema_path=args.schema) + validation_successful = schema_validation( + input_config, + json_schema, + config_path=args.filename, + schema_path=args.schema, + ) if not validation_successful: exit(SCHEMA_VALIDATION_FAILURE) From 680e92497a1a47c9f99e20e4295a8b25d82ca1d1 Mon Sep 17 00:00:00 2001 From: Paul Quiring Date: Tue, 17 Mar 2026 11:02:22 +0100 Subject: [PATCH 72/72] Use defined default values in configuration mapping --- scripts/config_mapping/lifecycle_config.py | 27 +++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 8812678f..73f238b5 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -16,8 +16,8 @@ "working_dir": "/tmp", "ready_recovery_action": { "restart": { - "number_of_attempts": 1, - "delay_before_restart": 0.5 + "number_of_attempts": 0, + "delay_before_restart": 0 } }, "recovery_action": { @@ -26,8 +26,8 @@ } }, "sandbox": { - "uid": 0, - "gid": 0, + "uid": 1000, + "gid": 1000, "supplementary_group_ids": [], "security_policy": "", "scheduling_policy": "SCHED_OTHER", @@ -35,23 +35,34 @@ } }, "component_properties": { + "binary_name": "", "application_profile": { - "application_type": "REPORTING", - "is_self_terminating": false + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } }, + "depends_on": [], + "process_arguments": [], "ready_condition": { "process_state": "Running" } }, "run_target": { - "transition_timeout": 5, + "description": "", + "depends_on": [], + "transition_timeout": 3, "recovery_action": { "switch_run_target": { "run_target": "fallback_run_target" } } }, - "alive_supervision" : { + "alive_supervision": { "evaluation_cycle": 0.5 }, "watchdog": {}