Hard-reload on integrity expiring during request;

Implement a way to hard-reload the application when the integrity token expires during a request, or inside its retry loop
This commit is contained in:
DevilXD committed 2022-09-17 18:28:26 +02:00
1 parent f65c0922c2
commit 7d14b51221
4 files changed
+56 -15

No files matched your search

+14 -1
View File
@@ -19,7 +19,20 @@ class ExitRequest(MinerException):
if args: if args:
super().__init__(*args) super().__init__(*args)
else: else:
super().__init__("Unknown error during request") super().__init__("Application was requested to exit")
class ReloadRequest(MinerException):
"""
Raised when the application is requested to reload by an expired request.
Intended for internal use only.
"""
def __init__(self, *args: object):
if args:
super().__init__(*args)
else:
super().__init__("Application was requested to reload its state")
class RequestException(MinerException): class RequestException(MinerException):
+1
View File
@@ -181,6 +181,7 @@ except Exception:
finally: finally:
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGTERM, signal.SIG_DFL) signal.signal(signal.SIGTERM, signal.SIG_DFL)
client.print(_("gui", "status", "exiting"))
loop.run_until_complete(client.shutdown()) loop.run_until_complete(client.shutdown())
if not client.gui.close_requested: if not client.gui.close_requested:
client.print(_("status", "terminated")) client.print(_("status", "terminated"))
+39 -12
View File
@@ -21,7 +21,7 @@ from gui import GUIManager
from channel import Channel from channel import Channel
from websocket import WebsocketPool from websocket import WebsocketPool
from inventory import DropsCampaign from inventory import DropsCampaign
from exceptions import MinerException, LoginException, CaptchaRequired, ExitRequest from exceptions import MinerException, LoginException, CaptchaRequired, ExitRequest, ReloadRequest
from utils import ( from utils import (
CHARS_HEX_LOWER, CHARS_HEX_LOWER,
timestamp, timestamp,
@@ -74,6 +74,20 @@ class _AuthState:
def integrity_expired(self) -> bool: def integrity_expired(self) -> bool:
return datetime.now(timezone.utc) >= self.integrity_expires return datetime.now(timezone.utc) >= self.integrity_expires
def _delattr(self, attr: str) -> None:
if hasattr(self, attr):
delattr(self, attr)
def clear(self) -> None:
self._delattr("user_id")
self._delattr("device_id")
self._delattr("session_id")
self._delattr("access_token")
self._delattr("client_version")
self._delattr("integrity_token")
self._delattr("integrity_expires")
self._logged_in.clear()
async def _login(self) -> str: async def _login(self) -> str:
logger.debug("Login flow started") logger.debug("Login flow started")
login_form: LoginForm = self._twitch.gui.login login_form: LoginForm = self._twitch.gui.login
@@ -302,7 +316,6 @@ class Twitch:
async def shutdown(self) -> None: async def shutdown(self) -> None:
start_time = time() start_time = time()
self.gui.print(_("gui", "status", "exiting"))
self.stop_watching() self.stop_watching()
if self._watching_task is not None: if self._watching_task is not None:
self._watching_task.cancel() self._watching_task.cancel()
@@ -310,13 +323,19 @@ class Twitch:
if self._mnt_task is not None: if self._mnt_task is not None:
self._mnt_task.cancel() self._mnt_task.cancel()
self._mnt_task = None self._mnt_task = None
# close session, save cookies and stop websocket # stop websocket, close session and save cookies
await self.websocket.stop(clear_topics=True)
if self._session is not None: if self._session is not None:
cookie_jar = cast(aiohttp.CookieJar, self._session.cookie_jar) cookie_jar = cast(aiohttp.CookieJar, self._session.cookie_jar)
cookie_jar.save(COOKIES_PATH) cookie_jar.save(COOKIES_PATH)
await self._session.close() await self._session.close()
self._session = None self._session = None
await self.websocket.stop() self._drop_update = None
self.games.clear()
self._drops.clear()
self.channels.clear()
self.inventory.clear()
self._auth_state.clear()
# wait at least half a second + whatever it takes to complete the closing # wait at least half a second + whatever it takes to complete the closing
# this allows aiohttp to safely close the session # this allows aiohttp to safely close the session
await asyncio.sleep(start_time + 0.5 - time()) await asyncio.sleep(start_time + 0.5 - time())
@@ -378,12 +397,17 @@ class Twitch:
return self.games[game] return self.games[game]
async def run(self): async def run(self):
try: while True:
await self._run() try:
except ExitRequest: await self._run()
pass except ReloadRequest:
except aiohttp.ContentTypeError as exc: await self.shutdown()
raise MinerException(_("login", "unexpected_content")) from exc continue
except ExitRequest:
pass
except aiohttp.ContentTypeError as exc:
raise MinerException(_("login", "unexpected_content")) from exc
break
async def _run(self): async def _run(self):
""" """
@@ -943,7 +967,7 @@ class Twitch:
@asynccontextmanager @asynccontextmanager
async def request( async def request(
self, method: str, url: str, **kwargs self, method: str, url: str, *, invalidate_after: datetime | None = None, **kwargs
) -> abc.AsyncIterator[aiohttp.ClientResponse]: ) -> abc.AsyncIterator[aiohttp.ClientResponse]:
session = await self.get_session() session = await self.get_session()
method = method.upper() method = method.upper()
@@ -953,6 +977,8 @@ class Twitch:
for delay in ExponentialBackoff(maximum=3*60): for delay in ExponentialBackoff(maximum=3*60):
if self.gui.close_requested: if self.gui.close_requested:
raise ExitRequest() raise ExitRequest()
elif invalidate_after is not None and datetime.now(timezone.utc) >= invalidate_after:
raise ReloadRequest()
try: try:
response: aiohttp.ClientResponse | None = None response: aiohttp.ClientResponse | None = None
done, pending = await asyncio.wait( done, pending = await asyncio.wait(
@@ -990,7 +1016,8 @@ class Twitch:
"POST", "POST",
"https://gql.twitch.tv/gql", "https://gql.twitch.tv/gql",
json=op, json=op,
headers=auth_state.gql_headers(integrity=True) headers=auth_state.gql_headers(integrity=True),
invalidate_after=auth_state.integrity_expires,
) as response: ) as response:
response_json: JsonType = await response.json() response_json: JsonType = await response.json()
gql_logger.debug(f"GQL Response: {response_json}") gql_logger.debug(f"GQL Response: {response_json}")
+2 -2
View File
@@ -19,7 +19,7 @@ from typing import (
import yarl import yarl
from constants import JsonType from constants import JsonType
from exceptions import ExitRequest from exceptions import ExitRequest, ReloadRequest
from constants import _resource_path as resource_path # noqa from constants import _resource_path as resource_path # noqa
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -77,7 +77,7 @@ def task_wrapper(
async def wrapper(*args: _P.args, **kwargs: _P.kwargs): async def wrapper(*args: _P.args, **kwargs: _P.kwargs):
try: try:
await afunc(*args, **kwargs) await afunc(*args, **kwargs)
except ExitRequest: except (ExitRequest, ReloadRequest):
pass pass
except Exception: except Exception:
logger.exception("Exception in task") logger.exception("Exception in task")