Skip to content

Fix custom Websocket for IPv6 connection#1153

Merged
Gatsik merged 8 commits intoFAForever:developfrom
Gatsik:patch-1
Feb 1, 2026
Merged

Fix custom Websocket for IPv6 connection#1153
Gatsik merged 8 commits intoFAForever:developfrom
Gatsik:patch-1

Conversation

@Gatsik
Copy link
Contributor

@Gatsik Gatsik commented Feb 1, 2026

Summary by CodeRabbit

  • New Features

    • Game titles can now be up to 128 characters.
  • Bug Fixes

    • Coop maps show a consistent placeholder icon.
    • Replay tracker is updated only after a replay successfully starts.
    • Replay access errors now show a user warning.
  • Improvements

    • Improved network/connection robustness and clearer error handling for streaming.
    • Map display layout refined for more consistent visual presentation.
    • Session restoration/initialization behavior adjusted.

✏️ Tip: You can customize this high-level summary in your review settings.

manage StreamWriter state instead of deleting and
recreating it every time
subclassing it and overriding `run` method makes `quit()` useless
some mapgen maps can have weird previews which are 'invisible'
server uses 128 in order to not break SQL inserts
there are no previews of them on the content server
and they spam logs with 404s
@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

Refactors websocket IO to a QObject-driven reader on a dedicated thread, removes SocketWriter, moves Websocket to compose reader/reader_thread and centralizes error handling; makes StreamWriter reusable with optional GameUrl and lifecycle helpers; small UI and replay-tracking control-flow tweaks.

Changes

Cohort / File(s) Summary
WebSocket architecture
src/client/workaround_websocket.py
Converted SocketReader from QThreadQObject with optional connection, added set_connection() and read(), changed error signal to carry an object. Removed SocketWriter. Websocket now creates reader_thread + reader, adds _start_read and sendMessageRequest signals, writes directly to self.connection, adds handle_error(), and simplifies sock_state handling and open/close lifecycle.
Replay streamer / writer lifecycle
src/fa/replaylivestreamer.py
StreamWriter ctor now accepts optional GameUrl, added is_finished(), reset(), set_game_url(); LiveReplayStreamer now instantiates a single reusable StreamWriter, wires signals once, adds on_api_error() and updates flow to reuse writer instead of recreating it.
Replay tracking timing
src/fa/replay.py
Move addition of replay_id to WatchedReplaysTracker to after runner.run succeeds (only update tracker when run returns true).
Map preview shortcut
src/fa/maps.py
Adds early return: if map name contains _coop_, preview function returns games/unknown_map.png immediately, bypassing further resolution.
UI constraints
src/games/gamepanelwidget.py, src/games/host_ui.py
Set mapLabel fixed height to 256px; increased game title input max length from 25 to 128 characters.
Client session restore behavior
src/client/_clientwindow.py
handle_welcome now only creates a new GameSession when self.game_session is None; the restoration path that relied on game_uid may be skipped if a session exists without game_uid.

Sequence Diagram(s)

sequenceDiagram
    participant WS as Websocket
    participant RT as ReaderThread
    participant SR as SocketReader
    participant CC as ClientConnection

    WS->>RT: create/start reader_thread
    WS->>SR: instantiate SocketReader(conn=None)
    WS->>RT: move SR to reader_thread
    WS->>SR: connect(_start_read → read)
    WS->>SR: connect(message_received → binaryMessageReceived)
    WS->>SR: connect(error → handle_error)

    WS->>WS: open() -> set self.connection
    WS->>SR: set_connection(self.connection)
    WS->>WS: emit _start_read
    RT->>SR: invoke read()
    SR->>CC: read data loop
    alt message received
        SR->>WS: emit message_received(data)
    else error
        SR->>WS: emit error(exception)
        WS->>WS: handle_error(exception)
    end
Loading
sequenceDiagram
    participant LRS as LiveReplayStreamer
    participant SW as StreamWriter
    participant API as ReplayAPI
    participant NP as NamedPipe/Server

    LRS->>SW: instantiate StreamWriter(gurl=None)
    LRS->>LRS: wire SW signals once

    LRS->>LRS: start_live_replay(gurl)
    LRS->>SW: set_game_url(gurl)
    LRS->>API: request replay access
    alt API success
        API->>LRS: return access_url
        LRS->>SW: connect_to_server(access_url)
        SW->>NP: establish named-pipe
        SW->>LRS: emit writer_ready
        LRS->>LRS: on_writer_ready() -> start replay
    else API error
        API->>LRS: error
        LRS->>LRS: on_api_error() -> show warning
    end

    Note over SW: reuse via reset() and set_game_url()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nibble threads and stitch a stream,
Signals hum and messages gleam.
A writer rests, then hops awake,
Maps and titles, tweaks I make.
Hooray — the rabbit did not break! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title focuses on IPv6 connection fixes, but the actual changes span websocket architecture refactoring, replay tracking, stream writer lifecycle, UI adjustments, and game session restoration logic—not primarily IPv6-related. Revise the title to reflect the main architectural changes such as 'Refactor websocket architecture and update connection handling' or provide context on how these changes specifically address IPv6 issues.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/fa/replaylivestreamer.py (1)

102-111: ⚠️ Potential issue | 🟡 Minor

Missing ERROR_PIPE_CONNECTED success case.

Per previous feedback on this file: when using win32pipe.ConnectNamedPipe, both a return value of 0 and ERROR_PIPE_CONNECTED (536870394 or winerror 535) should be treated as success. The latter occurs if a client connected between CreateNamedPipe and ConnectNamedPipe.

🛠️ Proposed fix
+        SYNCHRONOUS_SUCCESS = 0
+        ERROR_PIPE_CONNECTED = 535
         pipe_connected = cast(int, win32pipe.ConnectNamedPipe(pipe, None))

-        if pipe_connected != 0:
+        if pipe_connected not in (SYNCHRONOUS_SUCCESS, ERROR_PIPE_CONNECTED):
             self._logger.warning(

Based on learnings: "In src/fa/replaylivestreamer.py, when using pywin32's win32pipe.ConnectNamedPipe, treat both a return value of 0 (SYNCHRONOUS_SUCCESS) and ERROR_PIPE_CONNECTED as success."

🤖 Fix all issues with AI agents
In `@src/client/workaround_websocket.py`:
- Around line 110-120: The handle_error flow currently emits a NetworkError even
when close() was called intentionally; add a boolean flag (e.g.
self._closed_intentionally) on the connection class, set it True at the start of
close() and False after fully closed, and update handle_error(self, error) to
skip emitting errorOccurred and logging the error when that flag is True (but
still call close() to clean up). Ensure the flag is reset (False) after cleanup
so subsequent real errors are reported.
🧹 Nitpick comments (1)
src/client/workaround_websocket.py (1)

57-63: Thread lifecycle may need cleanup on destruction.

The reader_thread is started but never explicitly stopped in close(). While this allows thread reuse across connections, ensure the thread is properly terminated when the Websocket object is destroyed (e.g., in a destructor or cleanup method) to avoid resource leaks.

💡 Consider adding cleanup
def __del__(self) -> None:
    if self.reader_thread.isRunning():
        self.reader_thread.quit()
        self.reader_thread.wait()

Or better, provide an explicit shutdown() method to be called during application cleanup.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/workaround_websocket.py (1)

73-83: ⚠️ Potential issue | 🟠 Major

Missing early return after connection failure causes double error handling.

When all addresses fail to connect, errorOccurred is emitted but execution continues. The caller (open()) proceeds with an unconnected socket, leading to the websockets.connect() call failing with a separate exception. This results in inconsistent error handling.

🐛 Proposed fix
         else:
             self.errorOccurred.emit(QAbstractSocket.SocketError.NetworkError)
+            return

Then update open() to handle this:

     def open(self, url: QUrl) -> None:
         self.sock_state = State.CONNECTING
 
         self.connect()
-        assert self.socket is not None
+        if self.socket is None or not self._is_socket_connected():
+            return  # Error already emitted by connect()

Or alternatively, raise an exception from connect() when all attempts fail, and catch it in open().

🧹 Nitpick comments (1)
src/client/workaround_websocket.py (1)

56-62: Consider adding thread cleanup to prevent hangs on shutdown.

The reader_thread is created and started but never explicitly stopped or cleaned up. If the Websocket object is destroyed while the thread is running, this could cause the application to hang on exit or leak resources.

♻️ Proposed cleanup pattern

Add a cleanup method or destructor:

def cleanup(self) -> None:
    if self.reader_thread.isRunning():
        self.reader_thread.quit()
        self.reader_thread.wait()

Alternatively, connect the thread's cleanup to a parent object's destruction or call it explicitly in close() when the connection won't be reused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/_clientwindow.py (1)

1814-1828: ⚠️ Potential issue | 🟠 Major

Refresh GameSession when the logged-in user changes.

With the new condition, an existing GameSession is reused even if player_id/player_login no longer match the current login. That can happen on logout/login flows and would keep stale identity in the session. Consider recreating (or updating) the session when the user changes to avoid misattribution during game launch/restore.

✅ Suggested adjustment
-        if self.game_session is None:
+        if (
+            self.game_session is None
+            or self.game_session.player_id != self.id
+            or self.game_session.player_login != self.login
+        ):
             self.game_session = GameSession(
                 player_id=self.id,
                 player_login=self.login,
             )

@Gatsik Gatsik merged commit e357133 into FAForever:develop Feb 1, 2026
3 checks passed
@Gatsik Gatsik deleted the patch-1 branch February 1, 2026 20:10
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