Move asyncio loop handling to main.py
This commit is contained in:
1 parent
2fdbf809e7
commit
97d6ac1b29
4 files changed
+85
-56
No files matched your search
@@ -48,6 +48,7 @@ FORMATTER = logging.Formatter(
|
|||||||
|
|
||||||
class State(Enum):
|
class State(Enum):
|
||||||
IDLE = auto()
|
IDLE = auto()
|
||||||
|
EXIT = auto()
|
||||||
INVENTORY_FETCH = auto()
|
INVENTORY_FETCH = auto()
|
||||||
GAMES_UPDATE = auto()
|
GAMES_UPDATE = auto()
|
||||||
GAME_SELECT = auto()
|
GAME_SELECT = auto()
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ class GameSelector:
|
|||||||
highlightthickness=0,
|
highlightthickness=0,
|
||||||
)
|
)
|
||||||
self._list.pack(fill="both", expand=True)
|
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._games: OrderedDict[str, Game] = OrderedDict()
|
||||||
self._list.bind("<<ListboxSelect>>", self._on_select)
|
self._list.bind("<<ListboxSelect>>", self._on_select)
|
||||||
|
|
||||||
@@ -481,11 +481,11 @@ class ConsoleOutput:
|
|||||||
yscroll = ttk.Scrollbar(frame, orient="vertical")
|
yscroll = ttk.Scrollbar(frame, orient="vertical")
|
||||||
self._text = tk.Text(
|
self._text = tk.Text(
|
||||||
frame,
|
frame,
|
||||||
exportselection=False,
|
|
||||||
height=10,
|
|
||||||
width=52,
|
width=52,
|
||||||
|
height=10,
|
||||||
wrap="none",
|
wrap="none",
|
||||||
state="disabled",
|
state="disabled",
|
||||||
|
exportselection=False,
|
||||||
xscrollcommand=xscroll.set,
|
xscrollcommand=xscroll.set,
|
||||||
yscrollcommand=yscroll.set,
|
yscrollcommand=yscroll.set,
|
||||||
)
|
)
|
||||||
@@ -689,7 +689,7 @@ class TrayIcon:
|
|||||||
self.icon: Optional[pystray.Icon] = None
|
self.icon: Optional[pystray.Icon] = None
|
||||||
self._button = ttk.Button(master, command=self.minimize, text="Minimize to Tray")
|
self._button = ttk.Button(master, command=self.minimize, text="Minimize to Tray")
|
||||||
self._button.grid(column=0, row=0, sticky="e")
|
self._button.grid(column=0, row=0, sticky="e")
|
||||||
if manager._twitch._options.tray:
|
if manager._twitch.options.tray:
|
||||||
# start hidden in tray
|
# start hidden in tray
|
||||||
self._manager._root.after_idle(self.minimize)
|
self._manager._root.after_idle(self.minimize)
|
||||||
|
|
||||||
@@ -799,11 +799,11 @@ class GUIManager:
|
|||||||
root.update_idletasks()
|
root.update_idletasks()
|
||||||
root.minsize(width=0, height=root.winfo_reqheight())
|
root.minsize(width=0, height=root.winfo_reqheight())
|
||||||
# register logging handler
|
# register logging handler
|
||||||
handler = TKOutputHandler(self)
|
self._handler = TKOutputHandler(self)
|
||||||
handler.setFormatter(FORMATTER)
|
self._handler.setFormatter(FORMATTER)
|
||||||
logging.getLogger("TwitchDrops").addHandler(handler)
|
logging.getLogger("TwitchDrops").addHandler(self._handler)
|
||||||
# show the window when ready
|
# show the window when ready
|
||||||
if not self._twitch._options.tray:
|
if not self._twitch.options.tray:
|
||||||
self._root.deiconify()
|
self._root.deiconify()
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/56329342/tkinter-treeview-background-tag-not-working
|
# https://stackoverflow.com/questions/56329342/tkinter-treeview-background-tag-not-working
|
||||||
@@ -858,10 +858,21 @@ class GUIManager:
|
|||||||
self._poll_task = None
|
self._poll_task = None
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
"""
|
||||||
|
Requests the application to close.
|
||||||
|
The window itself will be closed in the closing sequence later.
|
||||||
|
"""
|
||||||
self._closed.set()
|
self._closed.set()
|
||||||
# notify client we're supposed to close
|
# notify client we're supposed to close
|
||||||
self._twitch.request_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):
|
def unfocus(self, event):
|
||||||
# support pressing ESC to unfocus
|
# support pressing ESC to unfocus
|
||||||
self._root.focus_set()
|
self._root.focus_set()
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import ctypes
|
import ctypes
|
||||||
|
import signal
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
|
import traceback
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from twitch import Twitch
|
from twitch import Twitch
|
||||||
from version import __version__
|
from version import __version__
|
||||||
|
from exceptions import CaptchaRequired
|
||||||
from constants import FORMATTER, LOG_PATH, WINDOW_TITLE
|
from constants import FORMATTER, LOG_PATH, WINDOW_TITLE
|
||||||
|
|
||||||
|
|
||||||
@@ -85,6 +89,33 @@ if options.log:
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
logging.getLogger("TwitchDrops.gql").setLevel(options.debug_gql)
|
logging.getLogger("TwitchDrops.gql").setLevel(options.debug_gql)
|
||||||
logging.getLogger("TwitchDrops.websocket").setLevel(options.debug_ws)
|
logging.getLogger("TwitchDrops.websocket").setLevel(options.debug_ws)
|
||||||
|
|
||||||
# client run
|
# client run
|
||||||
client = Twitch(options)
|
loop = asyncio.get_event_loop()
|
||||||
client.start()
|
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()
|
||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
from time import time
|
from time import time
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
@@ -75,8 +74,9 @@ class _AwaitableValue(Generic[_V]):
|
|||||||
|
|
||||||
|
|
||||||
class Twitch:
|
class Twitch:
|
||||||
def __init__(self, options: ParsedArgs):
|
def __init__(self, loop: asyncio.AbstractEventLoop, options: ParsedArgs):
|
||||||
self._options = options
|
self._loop = loop
|
||||||
|
self.options = options
|
||||||
# GUI
|
# GUI
|
||||||
self.gui = GUIManager(self)
|
self.gui = GUIManager(self)
|
||||||
# Cookies, session and auth
|
# Cookies, session and auth
|
||||||
@@ -84,6 +84,7 @@ class Twitch:
|
|||||||
if os.path.isfile(COOKIES_PATH):
|
if os.path.isfile(COOKIES_PATH):
|
||||||
cookie_jar.load(COOKIES_PATH)
|
cookie_jar.load(COOKIES_PATH)
|
||||||
self._session = aiohttp.ClientSession(
|
self._session = aiohttp.ClientSession(
|
||||||
|
loop=loop,
|
||||||
cookie_jar=cookie_jar,
|
cookie_jar=cookie_jar,
|
||||||
headers={"User-Agent": USER_AGENT},
|
headers={"User-Agent": USER_AGENT},
|
||||||
timeout=aiohttp.ClientTimeout(connect=5, total=10),
|
timeout=aiohttp.ClientTimeout(connect=5, total=10),
|
||||||
@@ -94,7 +95,7 @@ class Twitch:
|
|||||||
# State management
|
# State management
|
||||||
self._state: State = State.INVENTORY_FETCH
|
self._state: State = State.INVENTORY_FETCH
|
||||||
self._state_change = asyncio.Event()
|
self._state_change = asyncio.Event()
|
||||||
self.inventory: List[DropsCampaign] = [] # inventory
|
self.inventory: List[DropsCampaign] = []
|
||||||
# Storing and watching channels
|
# Storing and watching channels
|
||||||
self.channels: Dict[int, Channel] = {}
|
self.channels: Dict[int, Channel] = {}
|
||||||
self._watching_channel: _AwaitableValue[Channel] = _AwaitableValue()
|
self._watching_channel: _AwaitableValue[Channel] = _AwaitableValue()
|
||||||
@@ -103,13 +104,13 @@ class Twitch:
|
|||||||
self._drop_update: Optional[asyncio.Future[bool]] = None
|
self._drop_update: Optional[asyncio.Future[bool]] = None
|
||||||
# Websocket
|
# Websocket
|
||||||
self.websocket = WebsocketPool(self)
|
self.websocket = WebsocketPool(self)
|
||||||
# Runner task
|
|
||||||
self._main_task: Optional[asyncio.Task[None]] = None
|
|
||||||
|
|
||||||
def wait_until_login(self):
|
def wait_until_login(self):
|
||||||
return self._is_logged_in.wait()
|
return self._is_logged_in.wait()
|
||||||
|
|
||||||
def change_state(self, state: State) -> None:
|
def change_state(self, state: State) -> None:
|
||||||
|
if self._state is not State.EXIT:
|
||||||
|
# prevent state changing once we switch to exit state
|
||||||
self._state = state
|
self._state = state
|
||||||
self._state_change.set()
|
self._state_change.set()
|
||||||
|
|
||||||
@@ -118,58 +119,40 @@ class Twitch:
|
|||||||
# perfect for GUI usage
|
# perfect for GUI usage
|
||||||
return partial(self.change_state, state)
|
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):
|
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.
|
usually by the console or application window being closed.
|
||||||
"""
|
"""
|
||||||
self.stop()
|
self.change_state(State.EXIT)
|
||||||
|
|
||||||
def start(self):
|
def prevent_close(self):
|
||||||
self._loop = loop = asyncio.get_event_loop()
|
"""
|
||||||
self._main_task = loop.create_task(self._run())
|
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:
|
def print(self, *args, **kwargs):
|
||||||
loop.run_until_complete(self._main_task)
|
"""
|
||||||
except asyncio.CancelledError:
|
Can be used to print messages within the GUI.
|
||||||
# happens when the user requests close
|
"""
|
||||||
pass
|
self.gui.print(*args, **kwargs)
|
||||||
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 stop(self):
|
def stop(self):
|
||||||
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()
|
||||||
self._watching_task = None
|
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()
|
start_time = time()
|
||||||
self.gui.print("Exiting...")
|
self.gui.print("Exiting...")
|
||||||
self.stop()
|
self.stop()
|
||||||
@@ -186,7 +169,7 @@ class Twitch:
|
|||||||
watching_channel = self._watching_channel.get_with_default(None)
|
watching_channel = self._watching_channel.get_with_default(None)
|
||||||
return watching_channel is not None and watching_channel == channel
|
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.
|
Main method that runs the whole client.
|
||||||
|
|
||||||
@@ -322,6 +305,9 @@ class Twitch:
|
|||||||
self.stop_watching()
|
self.stop_watching()
|
||||||
self.gui.print(f"No suitable channel to watch for game: {selected_game}")
|
self.gui.print(f"No suitable channel to watch for game: {selected_game}")
|
||||||
self.change_state(State.IDLE)
|
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()
|
await self._state_change.wait()
|
||||||
|
|
||||||
@task_wrapper
|
@task_wrapper
|
||||||
|
|||||||
Reference in new issue
Block a user