From fcbfa325459fbef0ce8515d0f6ad1ae0ac480970 Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 29 Oct 2026 18:56:03 +0900 Subject: [PATCH] container: Stop gracefully on podman stop Containers used `sleep infinity` as PID 1, which never reacts to SIGTERM: the kernel does not apply default signal dispositions to PID 1, and sleep has no SIGTERM handler, so every `podman stop` silently ignored the signal, waited out the full 10-second grace period, and fell back to SIGKILL. PID 1 now traps TERM and INT itself and exits on request, so one-off manual stops of a still-running agent container end immediately instead of after the kill timeout. The PID 1 command is baked in at create time, so containers predating this change are detected and recreated automatically on the next run. --- CHANGELOG.md | 22 ++++++++++++++++++++++ agent-container.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b3aaa..38c6e6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Release notes +## 1.0.1 — 2026-10-29 + +`podman stop` now stops a running agent container immediately instead of +after a 10-second SIGTERM timeout followed by SIGKILL. + +### Why + +Containers used `sleep infinity` as PID 1. The kernel does not apply +default signal dispositions to PID 1, and `sleep` has no SIGTERM handler, +so the stop signal was silently ignored and every stop fell through to +SIGKILL. PID 1 now traps TERM (and INT) itself and exits on request: + +```sh +trap "exit 0" TERM INT; sleep infinity & wait +``` + +### Upgrading + +The PID 1 command is baked in when a container is created, so existing +containers are detected on the next run and recreated automatically. +Nothing in the container home is touched. + ## 1.0.0 — 2026-10-29 Containers now run with rootless Podman instead of Docker. diff --git a/agent-container.py b/agent-container.py index 1029344..c430f88 100755 --- a/agent-container.py +++ b/agent-container.py @@ -179,6 +179,12 @@ class AgentContainer: ) self.remove_container() + if self.container_exists() and not self._container_cmd_matches(): + logger.info( + "Recreating container: it predates the SIGTERM-aware PID 1" + ) + self.remove_container() + if not self.container_exists(): self.create_container() @@ -513,7 +519,11 @@ class AgentContainer: self.IMAGE, "sh", "-c", - "sleep infinity", + # PID 1 ignores signals with default dispositions, so a bare + # `sleep infinity` never reacts to SIGTERM and every + # `podman stop` times out into SIGKILL. Trapping TERM (and + # INT) makes stop requests end the container immediately. + 'trap "exit 0" TERM INT; sleep infinity & wait', ] ) @@ -555,6 +565,26 @@ class AgentContainer: ).stdout return "AGENT_CONTAINER=1" in env + def _container_cmd_matches(self) -> bool: + """Whether the container runs the current PID 1 command. + + The PID 1 entrypoint is baked in at create time and can only change + by recreating the container. Comparing against Config.Cmd catches + containers created before the SIGTERM-aware fix.""" + cmd = subprocess.run( + [ + self.RUNTIME, + "container", + "inspect", + "-f", + "{{json .Config.Cmd}}", + self.container_name, + ], + capture_output=True, + text=True, + ).stdout.strip() + return "TERM INT" in cmd + def start_container(self) -> bool: subprocess.run( [self.RUNTIME, "start", self.container_name],