From ee3f4d340a75b578446d7eeb5aa5004b5bbb4031 Mon Sep 17 00:00:00 2001 From: Mika C <59329994+Avnsx@users.noreply.github.com> Date: Mon, 17 Jul 2023 00:53:30 +0200 Subject: [PATCH 1/7] Fix stale websockets, breaking connector.stop() + fixes (https://github.com/sousa-andre/lcu-driver/issues/18) + Each WebSocket registered by the user is now stored in a list called self.connections. This allows us to iterate over each stored connection object later when the connector.stop() method is called. By unregistering each connection, the method can successfully close the connections as intended. This prevents the occurrence of an infinite loop, which could happen if the @connector.ws.register() decorator was used to register a connection before --- lcu_driver/connector.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lcu_driver/connector.py b/lcu_driver/connector.py index 694b85d..af35a92 100644 --- a/lcu_driver/connector.py +++ b/lcu_driver/connector.py @@ -35,12 +35,15 @@ class Connector(BaseConnector): def __init__(self, *, loop=None): super().__init__(loop) self._repeat_flag = True + self.connections = [] self.connection = None def register_connection(self, connection): + self.connections.append(self) self.connection = connection def unregister_connection(self, _): + self.connections.remove(self) self.connection = None @property @@ -78,6 +81,11 @@ async def stop(self) -> None: :rtype: None """ self._repeat_flag = False + + # close user-registered connections first, so listening to stale websockets won't cause an infinite loop + for e in self.connections: + self.unregister_connection(e) + if self.connection is not None: await self.connection._close() From 61e3ddfe72687aea170e9b7196d2d5eb93f6b6a0 Mon Sep 17 00:00:00 2001 From: Mika C <59329994+Avnsx@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:08:31 +0200 Subject: [PATCH 2/7] Fix a bug, which would break connector.start() + When the League Lobby Clients setting "Close client during game" was set to "Always"; it resulted in the in-game client being open exclusively during games. This caused the initialization of lcu-driver's connector.start() method to fail because the LeagueClientUx process was not running. + This change introduces automatically killing the in-game client, if it was detected as running during initialization of lcu-driver, allowing lcu-driver to start as intended. --- lcu_driver/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lcu_driver/utils.py b/lcu_driver/utils.py index f4ec764..3b222af 100644 --- a/lcu_driver/utils.py +++ b/lcu_driver/utils.py @@ -19,3 +19,6 @@ def _return_ux_process() -> Generator[Process, None, None]: if process.name() in ['LeagueClientUx.exe', 'LeagueClientUx']: yield process + + elif process.name() in ['League of Legends.exe', 'League of Legends']: + process.terminate() From d1bd0983a45ea89f6aa79c21745832978fffb7c4 Mon Sep 17 00:00:00 2001 From: Mika C <59329994+Avnsx@users.noreply.github.com> Date: Mon, 17 Jul 2023 02:40:33 +0200 Subject: [PATCH 3/7] fix ValueError + fix value error from being caused, if connector.stop() was called randomly somewhere in a function which didn't have the connector.close() function decorator --- lcu_driver/connector.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lcu_driver/connector.py b/lcu_driver/connector.py index af35a92..9482568 100644 --- a/lcu_driver/connector.py +++ b/lcu_driver/connector.py @@ -43,7 +43,10 @@ def register_connection(self, connection): self.connection = connection def unregister_connection(self, _): - self.connections.remove(self) + try: + self.connections.remove(self) + except ValueError: + pass self.connection = None @property From 4b81934a08101f103ee8aba32585ef11188d4772 Mon Sep 17 00:00:00 2001 From: Mika C <59329994+Avnsx@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:44:16 +0200 Subject: [PATCH 4/7] Reverting Modifications to Connector Class and Implementing Stop Event into MultipleClientConnector Class + Reversed the changes made to the Connector class due too many bugs encountered. + Implemented a stop event in the MultipleClientConnector class, enabling it to receive stop signals. Similar to the Connector class, this enhancement ensures proper termination and restart. Previously, attempting to exit & re-start this class in a loop would result in errors or it would become stuck instead of closing gracefully. --- lcu_driver/connector.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lcu_driver/connector.py b/lcu_driver/connector.py index 9482568..4a226ab 100644 --- a/lcu_driver/connector.py +++ b/lcu_driver/connector.py @@ -35,18 +35,12 @@ class Connector(BaseConnector): def __init__(self, *, loop=None): super().__init__(loop) self._repeat_flag = True - self.connections = [] self.connection = None def register_connection(self, connection): - self.connections.append(self) self.connection = connection def unregister_connection(self, _): - try: - self.connections.remove(self) - except ValueError: - pass self.connection = None @property @@ -84,11 +78,6 @@ async def stop(self) -> None: :rtype: None """ self._repeat_flag = False - - # close user-registered connections first, so listening to stale websockets won't cause an infinite loop - for e in self.connections: - self.unregister_connection(e) - if self.connection is not None: await self.connection._close() @@ -97,6 +86,7 @@ class MultipleClientConnector(BaseConnector): def __init__(self, *, loop=None): super().__init__(loop=loop) self.connections = [] + self.stop_event = asyncio.Event() # Event to signal stop def register_connection(self, connection): self.connections.append(connection) @@ -119,7 +109,7 @@ def _process_was_initialized(self, non_initialized_connection): async def _astart(self): tasks = [] try: - while True: + while not self.stop_event.is_set(): process_iter = _return_ux_process() process = next(process_iter, None) @@ -136,5 +126,10 @@ async def _astart(self): finally: await asyncio.gather(*tasks) + async def stop(self): + self.stop_event.set() + def start(self) -> None: + self.stop_event.clear() # Reset the stop event state self.loop.run_until_complete(self._astart()) + From c975fb3fecdab14aec45f196dea74b73c3afb648 Mon Sep 17 00:00:00 2001 From: Mika C <59329994+Avnsx@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:30:30 +0200 Subject: [PATCH 5/7] Fix JSON decoding & logging issue + addresses an issue in the WebSocket connection code where JSON decoding and logging were not handled correctly, resulting in errors and incomplete log messages, by utilising a f-string instead --- lcu_driver/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lcu_driver/connection.py b/lcu_driver/connection.py index 77c82e0..3ec5b09 100644 --- a/lcu_driver/connection.py +++ b/lcu_driver/connection.py @@ -184,7 +184,7 @@ async def run_ws(self): data = loads(msg.data)[2] self._connector.ws.match_event(self._connector, self, data) except JSONDecodeError: - logger.warning('Error decoding the following JSON: ', msg.data) + logger.warning(f'Error decoding the following JSON: {msg.data}') elif msg.type == aiohttp.WSMsgType.CLOSED: break From c0e1c15a720888295bf8de17be79ab5c308f979e Mon Sep 17 00:00:00 2001 From: Mika C <59329994+Avnsx@users.noreply.github.com> Date: Tue, 18 Jul 2023 01:01:28 +0200 Subject: [PATCH 6/7] fix a bug where MultipleClientConnector wouldn't stop + previously when a task started within the event loop, tried killing MCC, it wouldn't work + it's kind of a sloppy fix, to just add .stop into the finally clause. For a more robust design, sometime in the future it should be re-written using a context manager to close the connection rather than manually calling stop() there. + also ideally we should use asyncio.run() instead of managing the event loop's lifetime within the class itself anyway, so that python handles it --- lcu_driver/connector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lcu_driver/connector.py b/lcu_driver/connector.py index 4a226ab..86ab876 100644 --- a/lcu_driver/connector.py +++ b/lcu_driver/connector.py @@ -125,11 +125,12 @@ async def _astart(self): logger.info('Event loop interrupted by keyboard') finally: await asyncio.gather(*tasks) + await self.stop() async def stop(self): self.stop_event.set() def start(self) -> None: - self.stop_event.clear() # Reset the stop event state + self.stop_event.clear() # Reset the stop event state self.loop.run_until_complete(self._astart()) From 7f551843ed64068be946b6c176d8f3ede1b433f1 Mon Sep 17 00:00:00 2001 From: Mika C <59329994+Avnsx@users.noreply.github.com> Date: Fri, 28 Jul 2023 07:32:04 +0200 Subject: [PATCH 7/7] revert changes to utils - killing the in-game client this way, isn't really purposefull as I've originally intended --- lcu_driver/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lcu_driver/utils.py b/lcu_driver/utils.py index 3b222af..f4ec764 100644 --- a/lcu_driver/utils.py +++ b/lcu_driver/utils.py @@ -19,6 +19,3 @@ def _return_ux_process() -> Generator[Process, None, None]: if process.name() in ['LeagueClientUx.exe', 'LeagueClientUx']: yield process - - elif process.name() in ['League of Legends.exe', 'League of Legends']: - process.terminate()