Skip to content

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

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

MattL1989 wants to merge 2 commits 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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01K6zVKi1DtwmmBpy16MQyfY
Comment thread appium/webdriver/appium_service.py Outdated
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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, that wasn't intended. If the server didn't exit within 5.5 s after terminate(), its stderr was dropped. cab5a2d now kills it in that case and reads stderr again, still with a timeout so start() can't hang. If something else keeps the pipe open, it keeps whatever was read so far. I added unit tests for both cases.

If the Appium server did not exit within STOP_TIMEOUT_SEC after
terminate(), start() dropped its stderr, so the "Original error" part
of the AppiumServiceError message was lost.

Kill the process in that case and read stderr again, with the same
timeout, so start() still cannot hang. If another process still holds
stderr open, keep the output read so far.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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