Skip to content

Conversation

@vidonnus
Copy link

@vidonnus vidonnus commented Jan 2, 2026

Fix spurious socket error logging during intentional shutdown

Fixes #11

Problem

When the application shuts down (e.g., via Ctrl+C or calling terminate()), the socket/pipe threads are often blocked in recv() or recv_bytes() calls. When stop() closes the socket, this causes OSError: [Errno 9] Bad file descriptor (on Unix) or similar errors (on Windows) to be logged, even though this is expected behavior during intentional shutdown.

Example Error Log

2026-01-02 02:39:53,725 [   ERROR] mpv-jsonipc: Socket connection died.
Traceback (most recent call last):
  File "python_mpv_jsonipc.py", line 171, in run
    current_data = self.socket.recv(1024)
OSError: [Errno 9] Bad file descriptor

This error appears intermittently during normal shutdown due to a race condition:

  1. Main thread calls stop() → closes socket
  2. Socket thread is blocked in recv() → raises OSError when socket closes
  3. Exception handler logs it as an error

Impact

  • User confusion: Spurious error messages during normal operation
  • Log noise: Makes it harder to identify actual connection problems
  • False alarms: Monitoring systems may flag these as issues

Solution

Add a _stopping flag to both UnixSocket and WindowsSocket classes to distinguish between intentional shutdown and unexpected disconnection:

  1. Initialize flag: self._stopping = False in __init__()
  2. Set flag on shutdown: self._stopping = True in stop() before closing socket
  3. Conditional logging: In run() exception handler, only log if not self._stopping

Code Changes

UnixSocket class:

  • __init__: Added self._stopping = False (line 144)
  • stop(): Added self._stopping = True before closing (line 155)
  • run(): Changed error logging to if not self._stopping: (lines 193-195)

WindowsSocket class:

  • __init__: Added self._stopping = False (line 55)
  • stop(): Added self._stopping = True before closing (line 79)
  • run(): Changed error logging to if not self._stopping: (lines 121-123)

Why This Works

The flag is set before closing the socket, ensuring the socket thread sees it when the exception occurs. This creates a happens-before relationship:

Main Thread:              Socket Thread:
stop() called
  ↓
_stopping = True
  ↓
socket.close()
                          recv() raises OSError
                            ↓
                          Check _stopping flag
                            ↓
                          _stopping == True
                            ↓
                          Suppress error log ✓

Testing

Tested with jellyfin-mpv-shim on macOS (external MPV mode):

Test Scenarios

Ctrl+C shutdown: Clean exit, no "Bad file descriptor" error
Normal stop operations: No spurious errors logged
Playback stop/start cycles: Clean transitions
Unexpected errors preserved: Flag remains False for actual connection issues

Before Fix

2026-01-02 02:39:53,724 [    INFO] root: Stopping services...
2026-01-02 02:39:53,725 [   ERROR] mpv-jsonipc: Socket connection died.
Traceback (most recent call last):
  File "python_mpv_jsonipc.py", line 171, in run
    current_data = self.socket.recv(1024)
OSError: [Errno 9] Bad file descriptor

After Fix

2026-01-02 04:00:59,625 [    INFO] root: Stopping services...
2026-01-02 04:00:59,670 [    INFO] JELLYFIN.jellyfin_apiclient_python.ws_client: ---<[ websocket ]

Clean shutdown with no spurious errors! 🎉

Backward Compatibility

No breaking changes: Only internal implementation detail
No API changes: Public interface remains identical
No performance impact: Single boolean flag check
Thread-safe: Flag is set before socket operations

Alternative Approaches Considered

  1. Catch specific exception types: Doesn't work because OSError is legitimate for both cases
  2. Set socket to non-blocking: Would require significant refactoring
  3. Use socket timeout: Adds latency to shutdown
  4. Suppress all errors in stop(): Loses visibility of real problems

The flag-based approach is the cleanest solution that preserves error visibility while eliminating false positives.

Related Issues

This pattern is common in socket-based threading code. Similar fixes have been implemented in other projects:

  • Python's socketserver module uses shutdown flags
  • Many async I/O libraries use cancellation tokens

Checklist

  • Code changes implemented
  • Tested on Unix (macOS)
  • Tested with real-world application (jellyfin-mpv-shim)
  • No breaking changes
  • Commit message follows best practices
  • Maintainer review

When stop() is called, the socket/pipe thread may be blocked in recv().
Closing the socket raises OSError (Bad file descriptor on Unix, handle
errors on Windows) which is expected during intentional shutdown.

This commit adds a _stopping flag to both UnixSocket and WindowsSocket
classes to distinguish between:
- Intentional shutdown (stop() called): Suppress error logging
- Unexpected disconnection (MPV crash, etc): Continue logging errors

This eliminates spurious error messages during normal application
shutdown while preserving visibility of actual connection problems.

Fixes race condition where socket.close() in stop() causes recv() to
raise OSError in the socket thread's run() method.
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.

Disconnecting...

1 participant