Visitar URL original
fix: keep timeouts from crashing on Windows when SIGKILL is missing by DZH166 · Pull Request #8152 · google/adk-python · GitHub
Skip to content

fix: keep timeouts from crashing on Windows when SIGKILL is missing - #8152

Open
DZH166 wants to merge 1 commit into
google:mainfrom
DZH166:codex/windows-sigkill-timeout-20261112
Open

DZH166 wants to merge 1 commit into
google:mainfrom
DZH166:codex/windows-sigkill-timeout-20261112

Conversation

@DZH166

@DZH166 DZH166 commented Nov 14, 2026

Copy link
Copy Markdown

Link to Issue or Description of Change

No issue exists for this yet; the change is described below following the issue
template structure.

Problem:

A timed-out code execution or command crashes on Windows instead of being
killed and reported.

UnsafeLocalCodeExecutor._kill_execution escalates a timed-out execution with:

_signal_group(group, signal.SIGKILL)

signal.SIGKILL does not exist on Windows, so every execution that exceeds
timeout_seconds (the default is 300) raises
AttributeError: module 'signal' has no attribute 'SIGKILL' from the timeout
handler. The process is never killed (process.kill() on the next line never
runs), the agent sees the AttributeError instead of a
CodeExecutionResult with the timeout note, and the runaway code keeps
running. Three tests in tests/unittests/code_executors/test_unsafe_local_code_executor.py
already cover this path and fail on Windows with exactly this AttributeError;
CI only runs Linux, so the failure is invisible there.

LocalEnvironment._kill_command has the same unconditional
signal.SIGKILL lookup in its escalation step (SIGTERM, 5 s grace, then
SIGKILL), so a timed-out command whose descendants keep the output pipes open
crashes the same way instead of returning a timed-out ExecutionResult. Both
modules document Windows explicitly ("Ignored on Windows, as os.killpg is",
"terminate and kill below reach the execution itself on the platforms that
have no group to signal"), so the crash contradicts their own intent.

Solution:

Resolve the escalation signal the way cli/dev_server.py already does:

_signal_group(group, getattr(signal, 'SIGKILL', signal.SIGTERM))

_signal_group returns before using the signal value on platforms that have
no os.killpg, so the fallback is never actually sent by it; on POSIX,
signal.SIGKILL exists and behavior is unchanged. The module's own
process.kill() / proc.kill() continues to handle the process itself on
Windows, as the existing comments describe.

The one remaining failing test,
test_kill_execution_signals_group_before_killing_it, asserts the
POSIX group-signal order and references signal.SIGKILL in its own expected
list, so it cannot run on a platform without os.killpg; it is marked with
the repo's existing not hasattr(os, "killpg") skip convention (as
tests/unittests/environment/test_local_environment.py already does for the
same reason). The timeout-path regression coverage that runs everywhere is
test_execute_code_timeout and test_kill_execution_gives_up_on_pipes_that_never_close,
which went red→green with this fix on Windows.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Red→green on Windows 11, Python 3.13.9, repo state 287c0b0e65899b0060efd5672c8f419c1119530d:

  • Before the fix:
    • pytest tests/unittests/code_executors/test_unsafe_local_code_executor.py
      → 3 failed (test_execute_code_timeout,
      test_kill_execution_signals_group_before_killing_it,
      test_kill_execution_gives_up_on_pipes_that_never_close), each with
      AttributeError: module 'signal' has no attribute 'SIGKILL' at
      unsafe_local_code_executor.py:112.
    • Standalone repro of the same call path: UnsafeLocalCodeExecutor with
      timeout_seconds=2 running time.sleep(8) → ATTRIBUTEERROR.
    • Standalone repro of LocalEnvironment.execute(command, timeout=2.0) with
      a pipe-holding grandchild → ATTRIBUTEERROR.
  • After the fix:
    • pytest tests/unittests/code_executors/test_unsafe_local_code_executor.py
      → 26 passed, 4 skipped (3 pre-existing skips + the newly POSIX-marked
      ordering test).
    • Both standalone repros now return the normal timed-out result
      (timed_out=True, timeout note in stderr).
    • pytest tests/unittests/environment/test_local_environment.py → 15 passed,
      2 skipped (unchanged).
    • pytest tests/unittests/code_executors tests/unittests/environment
      (excluding the docker/kubernetes modules, whose extras this machine does
      not have) → 128 passed, 6 skipped.
    • git diff --check clean; pyink --check, ruff check, isort --check-only
      and the repo's scripts/compliance_checks.py clean on the changed files.

Manual End-to-End (E2E) Tests:

Not applicable beyond the repro above: the change only alters the
already-covered timeout-escalation path, and the repro exercises the exact
call (execute_code with a sleeping program; execute with a timed-out
command) on the affected platform.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

The timeout escalation in `UnsafeLocalCodeExecutor._kill_execution` and
`LocalEnvironment._kill_command` looked up `signal.SIGKILL` unconditionally.
Windows has no SIGKILL, so a timed-out code execution or command raised
`AttributeError: module 'signal' has no attribute 'SIGKILL'` instead of being
killed and reported: the runaway process kept running and the agent saw the
crash rather than the timeout result. Resolve the escalation signal with
`getattr(signal, 'SIGKILL', signal.SIGTERM)`, the same fallback
`dev_server.py` already uses; `_signal_group` no-ops on platforms without
process groups, so the fallback value is never sent by it, and on POSIX
nothing changes.

Mark `test_kill_execution_signals_group_before_killing_it` as needing POSIX
process groups: it asserts the group-signal order and references
`signal.SIGKILL` itself, so it cannot run on Windows. CI only runs Linux,
which is why the crash stayed invisible there.
@google-cla

google-cla Bot commented Nov 14, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant