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

@ -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],