Move asyncio loop handling to main.py

This commit is contained in:
DevilXD
2022-01-17 17:04:31 +01:00
parent 2fdbf809e7
commit 97d6ac1b29
4 changed files with 86 additions and 57 deletions

View File

@@ -48,6 +48,7 @@ FORMATTER = logging.Formatter(
class State(Enum):
IDLE = auto()
EXIT = auto()
INVENTORY_FETCH = auto()
GAMES_UPDATE = auto()
GAME_SELECT = auto()

27
gui.py
View File

@@ -260,7 +260,7 @@ class GameSelector:
highlightthickness=0,
)
self._list.pack(fill="both", expand=True)
self._selection: Optional[str] = self._manager._twitch._options.game
self._selection: Optional[str] = self._manager._twitch.options.game
self._games: OrderedDict[str, Game] = OrderedDict()
self._list.bind("<<ListboxSelect>>", self._on_select)
@@ -481,11 +481,11 @@ class ConsoleOutput:
yscroll = ttk.Scrollbar(frame, orient="vertical")
self._text = tk.Text(
frame,
exportselection=False,
height=10,
width=52,
height=10,
wrap="none",
state="disabled",
exportselection=False,
xscrollcommand=xscroll.set,
yscrollcommand=yscroll.set,
)
@@ -689,7 +689,7 @@ class TrayIcon:
self.icon: Optional[pystray.Icon] = None
self._button = ttk.Button(master, command=self.minimize, text="Minimize to Tray")
self._button.grid(column=0, row=0, sticky="e")
if manager._twitch._options.tray:
if manager._twitch.options.tray:
# start hidden in tray
self._manager._root.after_idle(self.minimize)
@@ -799,11 +799,11 @@ class GUIManager:
root.update_idletasks()
root.minsize(width=0, height=root.winfo_reqheight())
# register logging handler
handler = TKOutputHandler(self)
handler.setFormatter(FORMATTER)
logging.getLogger("TwitchDrops").addHandler(handler)
self._handler = TKOutputHandler(self)
self._handler.setFormatter(FORMATTER)
logging.getLogger("TwitchDrops").addHandler(self._handler)
# show the window when ready
if not self._twitch._options.tray:
if not self._twitch.options.tray:
self._root.deiconify()
# https://stackoverflow.com/questions/56329342/tkinter-treeview-background-tag-not-working
@@ -858,10 +858,21 @@ class GUIManager:
self._poll_task = None
def close(self):
"""
Requests the application to close.
The window itself will be closed in the closing sequence later.
"""
self._closed.set()
# notify client we're supposed to close
self._twitch.request_close()
def close_window(self):
"""
Closes the window. Invalidates the logger.
"""
self._root.destroy()
logging.getLogger("TwitchDrops").removeHandler(self._handler)
def unfocus(self, event):
# support pressing ESC to unfocus
self._root.focus_set()

35
main.py
View File

@@ -2,12 +2,16 @@ from __future__ import annotations
import sys
import ctypes
import signal
import asyncio
import logging
import argparse
import traceback
from typing import Optional
from twitch import Twitch
from version import __version__
from exceptions import CaptchaRequired
from constants import FORMATTER, LOG_PATH, WINDOW_TITLE
@@ -85,6 +89,33 @@ if options.log:
logger.addHandler(handler)
logging.getLogger("TwitchDrops.gql").setLevel(options.debug_gql)
logging.getLogger("TwitchDrops.websocket").setLevel(options.debug_ws)
# client run
client = Twitch(options)
client.start()
loop = asyncio.get_event_loop()
client = Twitch(loop, options)
signal.signal(signal.SIGINT, lambda *_: client.close())
signal.signal(signal.SIGTERM, lambda *_: client.close())
try:
loop.run_until_complete(client.run())
except CaptchaRequired:
client.prevent_close()
client.print(
"Your login attempt was denied by CAPTCHA.\nPlease try again in +12 hours."
)
except Exception:
client.prevent_close()
client.print("Fatal error encountered:\n")
client.print(traceback.format_exc())
finally:
signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGTERM, signal.SIG_DFL)
loop.run_until_complete(client.shutdown())
if not client.gui.close_requested:
client.print(
"\nApplication Terminated.\nClose the window to exit the application."
)
loop.run_until_complete(client.gui.wait_until_closed())
client.gui.stop()
client.gui.close_window()
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import os
import asyncio
import logging
import traceback
from yarl import URL
from time import time
from itertools import chain
@@ -75,8 +74,9 @@ class _AwaitableValue(Generic[_V]):
class Twitch:
def __init__(self, options: ParsedArgs):
self._options = options
def __init__(self, loop: asyncio.AbstractEventLoop, options: ParsedArgs):
self._loop = loop
self.options = options
# GUI
self.gui = GUIManager(self)
# Cookies, session and auth
@@ -84,6 +84,7 @@ class Twitch:
if os.path.isfile(COOKIES_PATH):
cookie_jar.load(COOKIES_PATH)
self._session = aiohttp.ClientSession(
loop=loop,
cookie_jar=cookie_jar,
headers={"User-Agent": USER_AGENT},
timeout=aiohttp.ClientTimeout(connect=5, total=10),
@@ -94,7 +95,7 @@ class Twitch:
# State management
self._state: State = State.INVENTORY_FETCH
self._state_change = asyncio.Event()
self.inventory: List[DropsCampaign] = [] # inventory
self.inventory: List[DropsCampaign] = []
# Storing and watching channels
self.channels: Dict[int, Channel] = {}
self._watching_channel: _AwaitableValue[Channel] = _AwaitableValue()
@@ -103,14 +104,14 @@ class Twitch:
self._drop_update: Optional[asyncio.Future[bool]] = None
# Websocket
self.websocket = WebsocketPool(self)
# Runner task
self._main_task: Optional[asyncio.Task[None]] = None
def wait_until_login(self):
return self._is_logged_in.wait()
def change_state(self, state: State) -> None:
self._state = state
if self._state is not State.EXIT:
# prevent state changing once we switch to exit state
self._state = state
self._state_change.set()
def state_change(self, state: State) -> Callable[[], None]:
@@ -118,58 +119,40 @@ class Twitch:
# perfect for GUI usage
return partial(self.change_state, state)
def close(self):
"""
Called when the application is requested to close by the operating system,
usually by receiving a SIGINT or SIGTERM.
"""
self.gui.close()
def request_close(self):
"""
Called when the application is requested to close,
Called when the application is requested to close by the user,
usually by the console or application window being closed.
"""
self.stop()
self.change_state(State.EXIT)
def start(self):
self._loop = loop = asyncio.get_event_loop()
self._main_task = loop.create_task(self._run())
def prevent_close(self):
"""
Called when the application window has to be prevented from closing, even after the user
closes it with X. Usually used solely to display tracebacks drom the closing sequence.
"""
self.gui.prevent_close()
try:
loop.run_until_complete(self._main_task)
except asyncio.CancelledError:
# happens when the user requests close
pass
except KeyboardInterrupt:
# KeyboardInterrupt causes run_until_complete to exit, but without cancelling the task.
# The loop stops and thus the task gets frozen, until the loop runs again.
# Because we don't want anything from there to actually run during cleanup,
# we need to explicitly cancel the task ourselves here.
self.stop()
except CaptchaRequired:
self.gui.prevent_close()
self.gui.print(
"Your login attempt was denied by CAPTCHA.\nPlease try again in +12 hours."
)
except Exception:
self.gui.prevent_close()
self.gui.print("Fatal error encountered:\n")
self.gui.print(traceback.format_exc())
finally:
loop.run_until_complete(self.close())
loop.run_until_complete(loop.shutdown_asyncgens())
if not self.gui.close_requested:
self.gui.print(
"\nApplication Terminated.\nClose the window to exit the application."
)
loop.run_until_complete(self.gui.wait_until_closed())
self.gui.stop()
loop.close()
def print(self, *args, **kwargs):
"""
Can be used to print messages within the GUI.
"""
self.gui.print(*args, **kwargs)
def stop(self):
self.stop_watching()
if self._watching_task is not None:
self._watching_task.cancel()
self._watching_task = None
if self._main_task is not None:
self._main_task.cancel()
self._main_task = None
async def close(self):
async def shutdown(self):
start_time = time()
self.gui.print("Exiting...")
self.stop()
@@ -186,7 +169,7 @@ class Twitch:
watching_channel = self._watching_channel.get_with_default(None)
return watching_channel is not None and watching_channel == channel
async def _run(self):
async def run(self):
"""
Main method that runs the whole client.
@@ -322,6 +305,9 @@ class Twitch:
self.stop_watching()
self.gui.print(f"No suitable channel to watch for game: {selected_game}")
self.change_state(State.IDLE)
elif self._state is State.EXIT:
# we've been requested to exit the application
break
await self._state_change.wait()
@task_wrapper