Files
TwitchDropsMiner/main.py
guihkx e31dcd85b4 linux: add truststore as a dependency
Because Linux distributions don't have an 'universal' path for the
system certificate store, bundling libssl built on distro A might not
work on distro B, because distros tend to change path to the
certificate store at build time.

This means that creating a PyInstaller package on Ubuntu will include
the libssl built there, and if you try to run this package on a
different distro (e.g. Arch Linux), any attempt to create an SSL
connection will fail, because Arch Linux has a different path for the
certificate store than Ubuntu.

This issue is usually worked around by bundling certificates with the
app (usually using certifi), and then pointing the SSL_CERT_FILE
environment to it.

However, truststore seems to be a better alternative, since it tries to
use the certificate store from the system instead.

The list of benefits are listed on the project's page on GitHub:

https://github.com/sethmlarson/truststore
2023-06-11 16:58:18 +02:00

202 lines
6.9 KiB
Python

from __future__ import annotations
# import an additional thing for proper PyInstaller freeze support
from multiprocessing import freeze_support
if __name__ == "__main__":
freeze_support()
import io
import sys
import signal
import asyncio
import logging
import argparse
import warnings
import traceback
import tkinter as tk
from tkinter import messagebox
from typing import IO, NoReturn
if sys.platform == "win32":
import win32gui
if sys.platform == "linux" and sys.version_info >= (3, 10):
import truststore
truststore.inject_into_ssl()
from translate import _
from twitch import Twitch
from settings import Settings
from version import __version__
from exceptions import CaptchaRequired
from utils import resource_path, set_root_icon
from constants import CALL, SELF_PATH, FILE_FORMATTER, LOG_PATH, WINDOW_TITLE
warnings.simplefilter("default", ResourceWarning)
class Parser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._message: io.StringIO = io.StringIO()
def _print_message(self, message: str, file: IO[str] | None = None) -> None:
self._message.write(message)
# print(message, file=self._message)
def exit(self, status: int = 0, message: str | None = None) -> NoReturn:
try:
super().exit(status, message) # sys.exit(2)
finally:
messagebox.showerror("Argument Parser Error", self._message.getvalue())
class ParsedArgs(argparse.Namespace):
_verbose: int
_debug_ws: bool
_debug_gql: bool
log: bool
tray: bool
no_run_check: bool
# TODO: replace int with union of literal values once typeshed updates
@property
def logging_level(self) -> int:
return {
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
3: CALL,
4: logging.DEBUG,
}[min(self._verbose, 3)]
@property
def debug_ws(self) -> int:
"""
If the debug flag is True, return DEBUG.
If the main logging level is DEBUG, return INFO to avoid seeing raw messages.
Otherwise, return NOTSET to inherit the global logging level.
"""
if self._debug_ws:
return logging.DEBUG
elif self._verbose >= 3:
return logging.INFO
return logging.NOTSET
@property
def debug_gql(self) -> int:
if self._debug_gql:
return logging.DEBUG
elif self._verbose >= 3:
return logging.INFO
return logging.NOTSET
# handle input parameters
# NOTE: parser output is shown via message box
# we also need a dummy invisible window for the parser
root = tk.Tk()
root.overrideredirect(True)
root.withdraw()
set_root_icon(root, resource_path("pickaxe.ico"))
root.update()
parser = Parser(
SELF_PATH.name,
description="A program that allows you to mine timed drops on Twitch.",
)
parser.add_argument("--version", action="version", version=f"v{__version__}")
parser.add_argument("-v", dest="_verbose", action="count", default=0)
parser.add_argument("--tray", action="store_true")
parser.add_argument("--log", action="store_true")
# undocumented debug args
parser.add_argument(
"--no-run-check", dest="no_run_check", action="store_true", help=argparse.SUPPRESS
)
parser.add_argument(
"--debug-ws", dest="_debug_ws", action="store_true", help=argparse.SUPPRESS
)
parser.add_argument(
"--debug-gql", dest="_debug_gql", action="store_true", help=argparse.SUPPRESS
)
args = parser.parse_args(namespace=ParsedArgs())
# load settings
try:
settings = Settings(args)
except Exception:
messagebox.showerror(
"Settings error",
f"There was an error while loading the settings file:\n\n{traceback.format_exc()}"
)
sys.exit(4)
# dummy window isn't needed anymore
root.destroy()
# get rid of unneeded objects
del root, parser
# check if we're not already running
if sys.platform == "win32":
try:
exists = win32gui.FindWindow(None, WINDOW_TITLE)
except AttributeError:
# we're not on Windows - continue
exists = False
if exists and not settings.no_run_check:
# already running - exit
sys.exit(3)
# set language
try:
_.set_language(settings.language)
except ValueError:
# this language doesn't exist - stick to English
pass
# handle logging stuff
if settings.logging_level > logging.DEBUG:
# redirect the root logger into a NullHandler, effectively ignoring all logging calls
# that aren't ours. This always runs, unless the main logging level is DEBUG or lower.
logging.getLogger().addHandler(logging.NullHandler())
logger = logging.getLogger("TwitchDrops")
logger.setLevel(settings.logging_level)
if settings.log:
handler = logging.FileHandler(LOG_PATH)
handler.setFormatter(FILE_FORMATTER)
logger.addHandler(handler)
logging.getLogger("TwitchDrops.gql").setLevel(settings.debug_gql)
logging.getLogger("TwitchDrops.websocket").setLevel(settings.debug_ws)
# client run
exit_status = 0
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = Twitch(settings)
signal.signal(signal.SIGINT, lambda *_: client.gui.close())
signal.signal(signal.SIGTERM, lambda *_: client.gui.close())
try:
loop.run_until_complete(client.run())
except CaptchaRequired:
exit_status = 1
client.prevent_close()
client.print(_("error", "captcha"))
except Exception:
exit_status = 1
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)
client.print(_("gui", "status", "exiting"))
loop.run_until_complete(client.shutdown())
if not client.gui.close_requested:
client.print(_("status", "terminated"))
client.gui.status.update(_("gui", "status", "terminated"))
loop.run_until_complete(client.gui.wait_until_closed())
# save the application state
# NOTE: we have to do it after wait_until_closed,
# because the user can alter some settings between app termination and closing the window
client.save(force=True)
client.gui.stop()
client.gui.close_window()
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
sys.exit(exit_status)