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.
This commit is contained in:
Jeena 2026-10-29 18:56:03 +09:00
parent 43dc757b92
commit fcbfa32545
2 changed files with 53 additions and 1 deletions

View file

@ -1,5 +1,27 @@
# Release notes # 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 ## 1.0.0 — 2026-10-29
Containers now run with rootless Podman instead of Docker. Containers now run with rootless Podman instead of Docker.

View file

@ -179,6 +179,12 @@ class AgentContainer:
) )
self.remove_container() 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(): if not self.container_exists():
self.create_container() self.create_container()
@ -513,7 +519,11 @@ class AgentContainer:
self.IMAGE, self.IMAGE,
"sh", "sh",
"-c", "-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 ).stdout
return "AGENT_CONTAINER=1" in env 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: def start_container(self) -> bool:
subprocess.run( subprocess.run(
[self.RUNTIME, "start", self.container_name], [self.RUNTIME, "start", self.container_name],