Skip to content

fix: prevent AppiumService.start from hanging when the server fails to start - #1342

Open
MattL1989 wants to merge 1 commit into
appium:masterfrom
MattL1989:claude/payment-received-46qabl
Open

MattL1989 wants to merge 1 commit into
appium:masterfrom
MattL1989:claude/payment-received-46qabl

Conversation

@MattL1989

Copy link
Copy Markdown
Contributor

Problem

AppiumService.start() is supposed to raise AppiumServiceError when the server does not start listening within timeout_ms. On macOS and Linux, where stderr defaults to subprocess.PIPE, it can hang forever instead.

When startup fails, the error path calls self._process.stderr.read() before stopping the process. If the server process is still running (for example, it started but listens on a different port, address or base path than the one parsed from the arguments, or it is slower than the timeout), read() waits for EOF on the pipe. That EOF only arrives when the process exits, so the call never returns and self.stop() is never reached.

Reproduction: start a fake server that writes to stderr and sleeps, with timeout_ms=1500. start() was still blocked after 20 seconds. With stderr=subprocess.DEVNULL the same call raised the expected error after 1.5 seconds.

Fix

In the error path, terminate the process first, then collect its stderr with communicate(timeout=...). The error message still includes the server output, as before. If the process ignores SIGTERM, the existing stop() still force-kills it.

STOP_TIMEOUT_SEC is added so start() and stop() share the same 5.5 second default.

Tests

  • Added a parametrized unit test covering both failure paths: the server is still running but not listening, and the server exited during startup. It asserts that the process is terminated before its output is collected, that stderr.read() is not called, and that the stderr output is still included in the error message. Both cases fail without the fix.
  • Checked with real processes:
    • A server that keeps running without listening now raises after 1.5 seconds, including its stderr output.
    • A server that exits during startup still has its stderr included in the error.
    • A server that ignores SIGTERM is killed, and the error is raised after about 12.5 seconds.
  • ruff check, ruff format --check, mypy appium and the full unit test suite (211 tests) pass.

🤖 Generated with Claude Code

…o start

When the server process did not start listening within the timeout,
start() read the whole stderr pipe before stopping the process. If the
server was still running, for example because it listened on another
URL, the read never returned and start() hung forever instead of raising
AppiumServiceError. This affected the default configuration on non-Windows
platforms, where stderr is piped.

Terminate the process before collecting its stderr with communicate(),
so the error is raised after the timeout and still includes the server
output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6zVKi1DtwmmBpy16MQyfY
try:
_, err_output = self._process.communicate(timeout=STOP_TIMEOUT_SEC)
except sp.TimeoutExpired:
err_output = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional to lose error message?

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.

2 participants