From 7b6babd9f5b3a4a8e1792fb1d9c86699b8188e90 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Mon, 18 May 2026 12:12:18 -0400 Subject: [PATCH 1/4] service: restart containers by exiting process --- repeater/service_utils.py | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/repeater/service_utils.py b/repeater/service_utils.py index 9a9c4a1..f565de9 100644 --- a/repeater/service_utils.py +++ b/repeater/service_utils.py @@ -6,11 +6,14 @@ Provides functions for service control operations like restart. import logging import os import subprocess +import threading +import time from typing import Dict, Optional, Tuple logger = logging.getLogger("ServiceUtils") INIT_SCRIPT = "/etc/init.d/S80pymc-repeater" BUILDROOT_METADATA_PATH = "/etc/pymc-image-build-id" +_CONTAINER_RESTART_DELAY_SECONDS = 1.0 def is_buildroot() -> bool: @@ -46,6 +49,40 @@ def get_buildroot_image_version() -> Optional[str]: return get_buildroot_image_info().get("image_version") +def is_container() -> bool: + """Detect common Docker/LXC/containerized environments.""" + if os.path.exists("/.dockerenv") or os.environ.get("container"): + return True + + try: + with open("/proc/1/environ", "rb") as handle: + if b"container=" in handle.read(): + return True + except (OSError, PermissionError): + pass + + try: + with open("/proc/1/cgroup", "r", encoding="utf-8") as handle: + cgroup_data = handle.read() + if any(token in cgroup_data for token in ("docker", "containerd", "kubepods", "lxc")): + return True + except OSError: + pass + + return os.path.exists("/run/host/container-manager") + + +def _schedule_container_exit(delay_seconds: float = _CONTAINER_RESTART_DELAY_SECONDS) -> None: + """Exit the current process shortly after returning success to the caller.""" + + def _exit_process() -> None: + time.sleep(delay_seconds) + logger.warning("Exiting repeater process to trigger container restart") + os._exit(0) + + threading.Thread(target=_exit_process, name="container-restart-exit", daemon=True).start() + + def restart_service() -> Tuple[bool, str]: """ Restart the pymc-repeater service. @@ -58,6 +95,11 @@ def restart_service() -> Tuple[bool, str]: Returns: Tuple[bool, str]: (success, message) """ + if is_container(): + _schedule_container_exit() + logger.info("Container environment detected; scheduled process exit for container restart") + return True, "Container restart initiated" + if is_buildroot(): if not os.path.exists(INIT_SCRIPT): logger.error("Buildroot init script not found: %s", INIT_SCRIPT) From ab8ae30e7d6eb8b8225246eebd65508232e24bc9 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Mon, 18 May 2026 12:40:43 -0400 Subject: [PATCH 2/4] web: clarify docker restart update messaging --- repeater/service_utils.py | 11 ++++++++++- repeater/web/update_endpoints.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/repeater/service_utils.py b/repeater/service_utils.py index f565de9..c52902d 100644 --- a/repeater/service_utils.py +++ b/repeater/service_utils.py @@ -83,6 +83,15 @@ def _schedule_container_exit(delay_seconds: float = _CONTAINER_RESTART_DELAY_SEC threading.Thread(target=_exit_process, name="container-restart-exit", daemon=True).start() +def get_container_restart_message() -> str: + """Return the user-facing restart message for containerized installs.""" + return ( + "Container restart initiated. " + "If you are running pyMC Repeater via Docker or Home Assistant, pull or rebuild " + "a newer image for packaged image updates to take effect." + ) + + def restart_service() -> Tuple[bool, str]: """ Restart the pymc-repeater service. @@ -98,7 +107,7 @@ def restart_service() -> Tuple[bool, str]: if is_container(): _schedule_container_exit() logger.info("Container environment detected; scheduled process exit for container restart") - return True, "Container restart initiated" + return True, get_container_restart_message() if is_buildroot(): if not os.path.exists(INIT_SCRIPT): diff --git a/repeater/web/update_endpoints.py b/repeater/web/update_endpoints.py index e167fc8..58d9fa8 100644 --- a/repeater/web/update_endpoints.py +++ b/repeater/web/update_endpoints.py @@ -29,7 +29,7 @@ from datetime import datetime from typing import List, Optional import cherrypy -from repeater.service_utils import is_buildroot +from repeater.service_utils import get_container_restart_message, is_buildroot, is_container logger = logging.getLogger("HTTPServer") @@ -891,7 +891,13 @@ def _do_install() -> None: restart_msg = str(exc) logger.warning(f"[Update] Could not restart service: {exc}") if restart_ok: - _state.finish_install(True, f"Upgraded to latest on channel '{channel}' – service restarted") + if is_container(): + _state.finish_install( + True, + f"Upgraded to latest on channel '{channel}' – {get_container_restart_message()}", + ) + else: + _state.finish_install(True, f"Upgraded to latest on channel '{channel}' – service restarted") else: _state.finish_install(False, f"Upgrade succeeded but service restart failed: {restart_msg}") else: From 11e2b90c18cb2659a6c7898e476da3f5c0b6839e Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Mon, 18 May 2026 12:43:19 -0400 Subject: [PATCH 3/4] ci: route docker publish by repository owner --- .github/workflows/docker-publish.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index b0f462c..c96b1a3 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -52,11 +52,26 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Determine image repository + id: image_repo + shell: bash + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.image_repository }}" ]; then + image_repository="${{ inputs.image_repository }}" + elif [ "${{ github.repository }}" = "yellowcooln/pyMC_Repeater" ]; then + image_repository="yellowcooln/pymc-repeater" + else + image_repository="pymcdev/pymc-repeater" + fi + + echo "Using image repository: ${image_repository}" + echo "image_repository=${image_repository}" >> "$GITHUB_OUTPUT" + - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: - images: ${{ github.event_name == 'workflow_dispatch' && inputs.image_repository || 'pymcdev/pymc-repeater' }} + images: ${{ steps.image_repo.outputs.image_repository }} tags: | type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} From f21aba0ac4354d8900a3f6f74857c2e7e900499d Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Mon, 18 May 2026 13:25:23 -0400 Subject: [PATCH 4/4] docker: mount config directory in compose --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3465abc..647db59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,5 +22,5 @@ services: group_add: - plugdev volumes: - - ./config.yaml:/etc/pymc_repeater/config.yaml + - ./config:/etc/pymc_repeater - ./data:/var/lib/pymc_repeater