From 3cba5a1612a2d609880266b9329f9b9821eb51d4 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Fri, 18 Sep 2026 06:08:30 +0900 Subject: [PATCH] refactor: extract Compose state parsing from shell --- libexec/mcserver-kit/compose-state.py | 38 ++++++++++++++++++++++++++ libexec/mcserver-kit/server-manager.sh | 22 +-------------- tests/run-tests.sh | 12 ++++++++ 3 files changed, 51 insertions(+), 21 deletions(-) create mode 100644 libexec/mcserver-kit/compose-state.py diff --git a/libexec/mcserver-kit/compose-state.py b/libexec/mcserver-kit/compose-state.py new file mode 100644 index 0000000..010486e --- /dev/null +++ b/libexec/mcserver-kit/compose-state.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Classify Docker Compose's JSON container listing for the CLI and TUI.""" + +import json +import sys + + +def classify(raw: str) -> str: + raw = raw.strip() + if not raw: + return "absent" + + try: + parsed = json.loads(raw) + except json.JSONDecodeError: + try: + parsed = [json.loads(line) for line in raw.splitlines()] + except json.JSONDecodeError: + return "unavailable" + + if isinstance(parsed, dict): + containers = [parsed] + elif isinstance(parsed, list) and all(isinstance(item, dict) for item in parsed): + containers = parsed + else: + return "unavailable" + + if any( + item.get("Service") == "minecraft" + and str(item.get("State", "")).lower() == "running" + for item in containers + ): + return "running" + return "stopped" if containers else "absent" + + +if __name__ == "__main__": + print(classify(sys.stdin.read())) diff --git a/libexec/mcserver-kit/server-manager.sh b/libexec/mcserver-kit/server-manager.sh index ddfc520..311abab 100755 --- a/libexec/mcserver-kit/server-manager.sh +++ b/libexec/mcserver-kit/server-manager.sh @@ -89,27 +89,7 @@ container_state() { printf 'unavailable\n' return fi - printf '%s\n' "$listing" | python3 -c ' -import json -import sys - -data = sys.stdin.read().strip() -if not data: - print("absent") - sys.exit() -try: - containers = json.loads(data) if data.startswith("[") else [json.loads(line) for line in data.splitlines()] -except (ValueError, TypeError): - print("unavailable") - sys.exit() -minecraft = [c for c in containers if c.get("Service") == "minecraft"] -if any(c.get("State", "").lower() == "running" for c in minecraft): - print("running") -elif containers: - print("stopped") -else: - print("absent") -' + printf '%s\n' "$listing" | python3 "${SCRIPT_DIR}/compose-state.py" } server_state() { diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 59cac5d..2833167 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -615,6 +615,17 @@ CONFIG assert_equal '600' "$(stat -c '%a' "$config_file")" 'config editor preserves restricted permissions' } +test_compose_state_parser() { + local parser="${REPO_ROOT}/libexec/mcserver-kit/compose-state.py" + assert_equal 'absent' "$(printf '\n' | python3 "$parser")" 'empty Compose output means no containers' + assert_equal 'absent' "$(printf '[]\n' | python3 "$parser")" 'an empty Compose array means no containers' + assert_equal 'running' "$(printf '[{"Service":"minecraft","State":"running"}]\n' | python3 "$parser")" 'array output detects the running server' + assert_equal 'stopped' "$(printf '{"Service":"minecraft","State":"exited"}\n' | python3 "$parser")" 'object output detects a retained container' + assert_equal 'running' "$(printf '%s\n%s\n' '{"Service":"playit","State":"running"}' '{"Service":"minecraft","State":"running"}' | python3 "$parser")" 'newline-delimited output finds the Minecraft container' + assert_equal 'unavailable' "$(printf '{invalid\n' | python3 "$parser")" 'malformed Compose JSON is not mistaken for no container' + assert_equal 'unavailable' "$(printf '123\n' | python3 "$parser")" 'unexpected Compose JSON shapes are rejected' +} + test_server_management() { local temp_dir="$1" local root="${temp_dir}/server-management/servers" @@ -957,6 +968,7 @@ main() { test_setup_command "$TEST_TEMP_DIR" test_reset_command "$TEST_TEMP_DIR" test_config_value_editor "$TEST_TEMP_DIR" + test_compose_state_parser test_server_management "$TEST_TEMP_DIR" test_server_property_editor "$TEST_TEMP_DIR" test_property_import_and_explorer "$TEST_TEMP_DIR"