Skip to content

Commit e357133

Browse files
authored
Merge pull request #1153 from Gatsik/patch-1
Fix custom Websocket for IPv6 connection
2 parents dcdc2e2 + af55355 commit e357133

File tree

7 files changed

+86
-98
lines changed

7 files changed

+86
-98
lines changed

src/client/_clientwindow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1811,7 +1811,7 @@ def handle_welcome(self, message: WelcomeCommand) -> None:
18111811

18121812
self.authorized.emit(self.me)
18131813

1814-
if self.game_session is None or self.game_session.game_uid is None:
1814+
if self.game_session is None:
18151815
self.game_session = GameSession(
18161816
player_id=self.id,
18171817
player_login=self.login,

src/client/workaround_websocket.py

Lines changed: 39 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import logging
22
import socket
3-
from queue import Queue
4-
from queue import ShutDown
53
from typing import cast
64

75
from PyQt6.QtCore import QByteArray
@@ -21,61 +19,48 @@
2119
logger = logging.getLogger(__name__)
2220

2321

24-
class SocketReader(QThread):
22+
class SocketReader(QObject):
2523
message_received = pyqtSignal(QByteArray)
26-
error = pyqtSignal()
24+
error = pyqtSignal(object)
2725

28-
def __init__(self, connection: ClientConnection) -> None:
26+
def __init__(self, connection: ClientConnection | None = None) -> None:
2927
super().__init__()
30-
self.connection = connection
28+
self.connection: ClientConnection | None = connection
3129

32-
def run(self) -> None:
30+
def set_connection(self, conn: ClientConnection, /) -> None:
31+
self.connection = conn
32+
33+
def read(self) -> None:
34+
if self.connection is None:
35+
logger.warning("Trying to read without any connection")
36+
return
3337
try:
3438
for message in self.connection:
3539
self.message_received.emit(QByteArray(cast(bytes, message)))
3640
except (ConnectionAbortedError, ConnectionClosedError) as e:
37-
logger.error(e)
38-
self.error.emit()
39-
return
40-
41-
42-
class SocketWriter(QThread):
43-
error = pyqtSignal()
44-
45-
def __init__(self, connection: ClientConnection, send_queue: Queue[bytes]) -> None:
46-
super().__init__()
47-
self.send_queue = send_queue
48-
self.connection = connection
49-
50-
def run(self) -> None:
51-
while True:
52-
try:
53-
item = self.send_queue.get()
54-
except ShutDown:
55-
return
56-
try:
57-
self.connection.send(item)
58-
except (ConnectionAbortedError, ConnectionClosedError) as e:
59-
logger.error(e)
60-
self.error.emit()
61-
return
62-
self.send_queue.task_done()
41+
self.error.emit(e)
6342

6443

6544
class Websocket(QObject):
6645
binaryMessageReceived = pyqtSignal(QByteArray)
6746
errorOccurred = pyqtSignal(QAbstractSocket.SocketError)
6847
stateChanged = pyqtSignal(QAbstractSocket.SocketState)
48+
_start_read = pyqtSignal()
6949

7050
def __init__(self, addresses: list[QHostAddress]) -> None:
7151
super().__init__()
7252
self.addresses = addresses
7353
self.socket: socket.socket | None = None
74-
self.send_queue: Queue[bytes] | None = None
7554
self._sock_state = QAbstractSocket.SocketState.UnconnectedState
7655

77-
self.reader_thread: SocketReader | None = None
78-
self.writer_thread: SocketWriter | None = None
56+
self.reader_thread = QThread()
57+
58+
self.reader = SocketReader()
59+
self.reader.moveToThread(self.reader_thread)
60+
self.reader.message_received.connect(self.binaryMessageReceived.emit)
61+
self.reader.error.connect(self.handle_error)
62+
self._start_read.connect(self.reader.read)
63+
7964
self.connection: ClientConnection | None = None
8065

8166
self._states = (
@@ -106,13 +91,14 @@ def sock_state(self, value: State) -> None:
10691
self._sock_state = self._states[value]
10792
self.stateChanged.emit(self._sock_state)
10893

109-
def sync_state(self) -> None:
110-
assert self.connection is not None
111-
self.sock_state = self.connection.state
112-
11394
def sendBinaryMessage(self, message: bytes) -> None:
114-
assert self.send_queue is not None
115-
self.send_queue.put(message)
95+
if self.connection is not None:
96+
try:
97+
self.connection.send(message)
98+
except (ConnectionAbortedError, ConnectionClosedError) as e:
99+
self.handle_error(e)
100+
else:
101+
logger.warning("Trying to write without any connection")
116102

117103
def state(self) -> QAbstractSocket.SocketState:
118104
return self._sock_state
@@ -121,30 +107,19 @@ def errorString(self) -> str:
121107
return "[Not implemented]"
122108

123109
def close(self) -> None:
124-
self._sock_state = QAbstractSocket.SocketState.UnconnectedState
125-
self.stateChanged.emit(self._sock_state)
126-
127-
if self.send_queue is not None:
128-
self.send_queue.shutdown()
129-
self.send_queue = None
130-
if self.reader_thread is not None:
131-
self.reader_thread.quit()
132-
self.reader_thread = None
133-
if self.writer_thread is not None:
134-
self.writer_thread.quit()
135-
self.writer_thread = None
136110
if self.connection is not None:
137111
self.connection.close()
138-
self.connection = None
112+
self.connection = None
139113
self.socket = None
114+
self.sock_state = State.CLOSED
140115

141-
def reader_writer_error(self) -> None:
142-
self.sync_state()
116+
def handle_error(self, error: Exception) -> None:
117+
logger.error(error)
143118
self.errorOccurred.emit(QAbstractSocket.SocketError.NetworkError)
119+
self.close()
144120

145121
def open(self, url: QUrl) -> None:
146-
self._sock_state = QAbstractSocket.SocketState.ConnectingState
147-
self.stateChanged.emit(self._sock_state)
122+
self.sock_state = State.CONNECTING
148123

149124
self.connect()
150125
assert self.socket is not None
@@ -158,18 +133,9 @@ def open(self, url: QUrl) -> None:
158133
self.connection.debug = False
159134
self.connection.protocol.debug = False
160135

161-
self.start_read_write()
162-
self.sync_state()
136+
self.reader.set_connection(self.connection)
137+
if not self.reader_thread.isRunning():
138+
self.reader_thread.start()
139+
self._start_read.emit()
163140

164-
def start_read_write(self) -> None:
165-
assert self.connection is not None
166-
167-
self.reader_thread = SocketReader(self.connection)
168-
self.reader_thread.message_received.connect(self.binaryMessageReceived.emit)
169-
self.reader_thread.error.connect(self.reader_writer_error)
170-
self.reader_thread.start()
171-
172-
self.send_queue = Queue()
173-
self.writer_thread = SocketWriter(self.connection, self.send_queue)
174-
self.writer_thread.error.connect(self.reader_writer_error)
175-
self.writer_thread.start()
141+
self.sock_state = self.connection.state

src/fa/maps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ def preview(
364364
):
365365
logger.debug("Using fresh preview image for: " + mapname)
366366
return util.THEME.icon(img['cache'], False, pixmap)
367+
368+
if "_coop_" in mapname:
369+
return util.THEME.icon("games/unknown_map.png")
370+
367371
except Exception:
368372
logger.debug("Map Preview Exception ('%s')", mapname, exc_info=sys.exc_info())
369373
return None

src/fa/replay.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ def replay(source, detach=False):
199199
if replay_id:
200200
arguments.append("/replayid")
201201
arguments.append(str(replay_id))
202-
WatchedReplaysTracker.add(replay_id)
203202

204203
# Update the game appropriately
205204
if not check(mod, mapname, version, featured_mod_versions, sim_mods, game_dir):
@@ -209,6 +208,8 @@ def replay(source, detach=False):
209208

210209
if runner.run(None, arguments, detach):
211210
logger.info("Viewing Replay.")
211+
if replay_id:
212+
WatchedReplaysTracker.add(replay_id)
212213
return True
213214
else:
214215
logger.error("Replaying failed. Guru meditation: %s", arguments)

src/fa/replaylivestreamer.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from PyQt6.QtCore import QUrl
1212
from PyQt6.QtCore import pyqtSignal
1313
from PyQt6.QtNetwork import QAbstractSocket
14+
from PyQt6.QtNetwork import QNetworkReply
1415
from PyQt6.QtWebSockets import QWebSocket
1516
from PyQt6.QtWidgets import QMessageBox
1617

@@ -37,7 +38,7 @@ class StreamWriter(QObject):
3738
_close = pyqtSignal()
3839
_abort = pyqtSignal()
3940

40-
def __init__(self, gurl: GameUrl) -> None:
41+
def __init__(self, gurl: GameUrl | None = None) -> None:
4142
super().__init__()
4243
self.game_url = gurl
4344

@@ -46,8 +47,6 @@ def __init__(self, gurl: GameUrl) -> None:
4647
self.socket.connected.connect(self.on_socket_connected)
4748
self.socket.disconnected.connect(self.on_socket_disconnected)
4849

49-
self.pipe_connected: int | None = None
50-
5150
self.queue: Queue[bytes] = Queue()
5251
self._thread = Thread(target=self.stream, daemon=True)
5352

@@ -72,6 +71,22 @@ def __init__(self, gurl: GameUrl) -> None:
7271

7372
self._close_intended = False
7473

74+
def is_finished(self) -> bool:
75+
return self._finished
76+
77+
def reset(self) -> None:
78+
self.game_url = None
79+
80+
self._ready = False
81+
self._finished = True
82+
self._close_intended = False
83+
84+
self.queue = Queue()
85+
self._thread = Thread(target=self.stream, daemon=True)
86+
87+
def set_game_url(self, gurl: GameUrl, /) -> None:
88+
self.game_url = gurl
89+
7590
def start(self) -> None:
7691
self._finished = False
7792
self._logger.debug("Starting named pipe thread...")
@@ -84,12 +99,12 @@ def stream(self) -> None:
8499
win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_WAIT,
85100
1, 65536, 65536, 0, win32security.SECURITY_ATTRIBUTES(),
86101
)
87-
self.pipe_connected = cast(int, win32pipe.ConnectNamedPipe(pipe, None))
102+
pipe_connected = cast(int, win32pipe.ConnectNamedPipe(pipe, None))
88103

89-
if self.pipe_connected != 0:
104+
if pipe_connected != 0:
90105
self._logger.warning(
91106
"Coul not connect named pipe. ConnectNamedPipe returned %s",
92-
self.pipe_connected,
107+
pipe_connected,
93108
)
94109
win32file.CloseHandle(pipe)
95110
self._close.emit()
@@ -119,6 +134,7 @@ def on_socket_connected(self) -> None:
119134
# in the past presumably to identify from who we want to receive replay data.
120135
# nowadays replay server merges data from all players, but the format remained
121136
# the same, because the game uses this format
137+
assert self.game_url is not None
122138
msg = f"G/{self.game_url.uid}/{self.game_url.player}.scfareplay\x00".encode()
123139
self.socket.sendBinaryMessage(msg)
124140

@@ -150,8 +166,6 @@ def close(self) -> None:
150166

151167
def shutdown(self) -> None:
152168
self.queue.shutdown()
153-
self.pipe_connected = None
154-
155169
self._finished = True
156170

157171
self.closed.emit()
@@ -167,32 +181,34 @@ def __init__(self) -> None:
167181
self.api = UserApiAccessor()
168182

169183
self.game_url: GameUrl | None = None
170-
self.writer: StreamWriter | None = None
184+
185+
self.writer = StreamWriter()
186+
self.writer.ready.connect(self.on_writer_ready)
187+
self.writer.not_ready.connect(self.on_writer_not_ready)
188+
self.writer.closed.connect(self.on_writer_closed)
171189

172190
def start_live_replay(self, gurl: GameUrl) -> None:
173191
# TODO: handle linux too
174192
if sys.platform == "win32" and Settings.get("game/pipe_live_replay", True, type=bool):
193+
if not self.writer.is_finished():
194+
self._logger.warning("Another instance of StreamWriter is not finished yet.")
195+
QMessageBox.warning(None, "Live Replay", "Another live replay is already running.")
196+
return
197+
175198
self.game_url = gurl
176-
self.api.get("/replay/access", self.on_replay_access_url)
199+
self.writer.set_game_url(gurl)
200+
self.api.get("/replay/access", self.on_replay_access_url, self.on_api_error)
177201
else:
178202
replay(gurl)
179203

180204
def on_replay_access_url(self, data: dict[str, Any]) -> None:
181-
if self.writer is not None:
182-
self._logger.warning("Another instance of StreamWriter is not finished yet.")
183-
QMessageBox.warning(None, "Live Replay", "Another live replay is already running.")
184-
return
185-
186-
assert self.game_url is not None
187-
self.writer = StreamWriter(self.game_url)
188-
self.writer.ready.connect(self.on_writer_ready)
189-
self.writer.not_ready.connect(self.on_writer_not_ready)
190-
self.writer.closed.connect(self.on_writer_closed)
191205
self.writer.connect_to_replay_server(QUrl(data["accessUrl"]))
192206

207+
def on_api_error(self, _: QNetworkReply) -> None:
208+
QMessageBox.warning(None, "Live Replay", "Could not get access to replay server")
209+
193210
def on_writer_ready(self) -> None:
194211
assert self.game_url is not None
195-
assert self.writer is not None
196212
if replay(self.game_url):
197213
self.writer.start()
198214
else:
@@ -204,4 +220,4 @@ def on_writer_not_ready(self) -> None:
204220
self.on_writer_closed()
205221

206222
def on_writer_closed(self) -> None:
207-
self.writer = None
223+
self.writer.reset()

src/games/gamepanelwidget.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def setupUi(self, widget: QWidget) -> None:
109109
layout = QVBoxLayout(widget)
110110
self.mapLabel = ClickableLabel()
111111
self.mapLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
112+
self.mapLabel.setFixedHeight(256)
112113
self.getMapButton = QPushButton("Download/Generate map")
113114

114115
self.titleLabel = QLabel()

src/games/host_ui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def setupUi(self, widget: QWidget) -> None:
3434

3535
self.titleEdit = QLineEdit()
3636
self.titleEdit.setFont(QFont("Segoe UI", 11))
37-
self.titleEdit.setMaxLength(25)
37+
self.titleEdit.setMaxLength(128)
3838
self.titleEdit.setPlaceholderText("[REQUIRED] Enter game name...")
3939

4040
options_layout = QHBoxLayout()

0 commit comments

Comments
 (0)