diff --git a/harmonyqml/__init__.py b/harmonyqml/__init__.py index 7ea42a73..8ecb3806 100644 --- a/harmonyqml/__init__.py +++ b/harmonyqml/__init__.py @@ -4,8 +4,6 @@ import os import sys -# logging.basicConfig(level=logging.INFO) - # The disk cache is responsible for multiple display bugs when running # the app for the first time/when cache needs to be recompiled, on top # of litering the source folders with .qmlc files. @@ -13,5 +11,11 @@ os.environ["QML_DISABLE_DISK_CACHE"] = "1" def run() -> None: - from . import app - _ = app.Application(sys.argv) + from .app import Application + app = Application(sys.argv) + + from .engine import Engine + engine = Engine(debug=app.debug) + engine.showWindow() + + sys.exit(app.exec_()) diff --git a/harmonyqml/app.py b/harmonyqml/app.py index 966a7f2e..5210d801 100644 --- a/harmonyqml/app.py +++ b/harmonyqml/app.py @@ -10,17 +10,13 @@ from . import __about__ class Application(QGuiApplication): def __init__(self, args: Optional[List[str]] = None) -> None: - try: - args.index("--debug") # type: ignore - debug = True - except (AttributeError, ValueError): - debug = False + self.debug = False + + if args and "--debug" in args: + del args[args.index("--debug")] + self.debug = True super().__init__(args or []) self.setApplicationName(__about__.__pkg_name__) self.setApplicationDisplayName(__about__.__pretty_name__) - - from .engine import Engine - engine = Engine(self, debug=debug) - engine.show_window() diff --git a/harmonyqml/backend/client.py b/harmonyqml/backend/client.py index e9471087..24e74e2f 100644 --- a/harmonyqml/backend/client.py +++ b/harmonyqml/backend/client.py @@ -13,11 +13,6 @@ import nio from .network_manager import NetworkManager from .pyqt_future import futurize -# One pool per hostname/remote server; -# multiple Client for different accounts on the same server can exist. -_POOLS: DefaultDict[str, ThreadPoolExecutor] = \ - DefaultDict(lambda: ThreadPoolExecutor(max_workers=6)) - class Client(QObject): roomInvited = pyqtSignal([str, dict], [str]) @@ -36,9 +31,9 @@ class Client(QObject): def __init__(self, manager, - hostname: str, - username: str, - device_id: str = "") -> None: + hostname: str, + username: str, + device_id: str = "") -> None: super().__init__(manager) self.manager = manager @@ -46,7 +41,7 @@ class Client(QObject): self.host: str = host self.port: int = int(port[0]) if port else 443 - self.pool: ThreadPoolExecutor = _POOLS[self.host] + self.pool: ThreadPoolExecutor = ThreadPoolExecutor(6) self.nio: nio.client.HttpClient = \ nio.client.HttpClient(self.host, username, device_id) @@ -109,9 +104,12 @@ class Client(QObject): @futurize(pyqt=False) def startSyncing(self) -> None: while True: - self._on_sync(self.net_sync.talk( - self.nio_sync.sync, timeout=8000 - )) + try: + response = self.net_sync.talk(self.nio_sync.sync, timeout=8000) + except nio.LocalProtocolError: # logout occured + pass + else: + self._on_sync(response) if self._stop_sync.is_set(): self._stop_sync.clear() diff --git a/harmonyqml/backend/client_manager.py b/harmonyqml/backend/client_manager.py index c423c07c..8e98fc7a 100644 --- a/harmonyqml/backend/client_manager.py +++ b/harmonyqml/backend/client_manager.py @@ -85,6 +85,13 @@ class ClientManager(QObject): client.logout() + @pyqtSlot() + def deleteAll(self) -> None: + for user_id in self.clients.copy(): + self.delete(user_id) + print("deleted", user_id, self.clients) + + @pyqtProperty(str, constant=True) def defaultDeviceName(self) -> str: # pylint: disable=no-self-use os_ = f" on {platform.system()}".rstrip() diff --git a/harmonyqml/backend/pyqt_future.py b/harmonyqml/backend/pyqt_future.py index 9045cdee..b6ee516d 100644 --- a/harmonyqml/backend/pyqt_future.py +++ b/harmonyqml/backend/pyqt_future.py @@ -80,7 +80,7 @@ def futurize(max_instances: Optional[int] = None, pyqt: bool = True return func(self, *args, **kws) except Exception: traceback.print_exc() - logging.error("Exiting thread due to exception.") + logging.error("Exiting thread/process due to exception.") sys.exit(1) finally: del _RUNNING[_RUNNING.index((self.pool, func, args, kws))] diff --git a/harmonyqml/components/Chat/Chat.qml b/harmonyqml/components/Chat/Chat.qml index 2a2f0650..d7ec5be0 100644 --- a/harmonyqml/components/Chat/Chat.qml +++ b/harmonyqml/components/Chat/Chat.qml @@ -4,6 +4,7 @@ import "Banners" import "RoomEventList" HColumnLayout { + Component.onCompleted: Backend.pdb() property string userId: "" property string roomId: "" diff --git a/harmonyqml/components/Window.qml b/harmonyqml/components/Window.qml index 21e759a5..46eb850d 100644 --- a/harmonyqml/components/Window.qml +++ b/harmonyqml/components/Window.qml @@ -8,6 +8,8 @@ ApplicationWindow { width: Math.min(Screen.width, 1152) height: Math.min(Screen.height, 768) + onClosing: Backend.clientManager.deleteAll() + Loader { anchors.fill: parent source: "UI.qml" diff --git a/harmonyqml/engine.py b/harmonyqml/engine.py index 31e77e37..e6336e0e 100644 --- a/harmonyqml/engine.py +++ b/harmonyqml/engine.py @@ -1,57 +1,54 @@ # Copyright 2019 miruka # This file is part of harmonyqml, licensed under GPLv3. -import logging +import signal import sys from pathlib import Path -from typing import Generator +from typing import Any, Dict, Generator -from PyQt5.QtCore import QFileSystemWatcher, QObject, QTimer +from PyQt5.QtCore import QObject, QTimer from PyQt5.QtQml import QQmlApplicationEngine -from .app import Application -from .backend.backend import Backend - -# logging.basicConfig(level=logging.INFO) - class Engine(QQmlApplicationEngine): - def __init__(self, - app: Application, - debug: bool = False) -> None: - super().__init__(app) - self.app = app - self.backend = Backend(self) - self.app_dir = Path(sys.argv[0]).resolve().parent + def __init__(self, debug: bool = False) -> None: + # Connect UNXI signals to properly exit program + self._original_signal_handlers: Dict[int, Any] = {} - # Set QML properties - self.rootContext().setContextProperty("Engine", self) - self.rootContext().setContextProperty("Backend", self.backend) - - # Connect Qt signals - self.quit.connect(self.app.quit) + for signame in ("INT" , "HUP", "QUIT", "TERM"): + sig = signal.Signals[f"SIG{signame}"] # pylint: disable=no-member + self._original_signal_handlers[sig] = signal.getsignal(sig) + signal.signal(sig, self.onExitSignal) # Make SIGINT (ctrl-c) work self._sigint_timer = QTimer() self._sigint_timer.timeout.connect(lambda: None) self._sigint_timer.start(100) + super().__init__() + self.app_dir = Path(sys.argv[0]).resolve().parent + + from .backend.backend import Backend + self.backend = Backend(self) + self.rootContext().setContextProperty("Backend", self.backend) + # Setup UI live-reloading when a file is edited if debug: + from PyQt5.QtCore import QFileSystemWatcher self._watcher = QFileSystemWatcher() - self._watcher.directoryChanged.connect(lambda _: self.reload_qml()) + self._watcher.directoryChanged.connect(lambda _: self.reloadQml()) self._watcher.addPath(str(self.app_dir)) for _dir in list(self._recursive_dirs_in(self.app_dir)): self._watcher.addPath(str(_dir)) - # Load QML page and show window - self.load(str(self.app_dir / "components" / "Window.qml")) + def onExitSignal(self, *_) -> None: + for sig, handler in self._original_signal_handlers.items(): + signal.signal(sig, handler) - def show_window(self) -> None: - self.rootObjects()[0].show() - sys.exit(self.app.exec()) + self._original_signal_handlers.clear() + self.closeWindow() def _recursive_dirs_in(self, path: Path) -> Generator[Path, None, None]: @@ -61,10 +58,22 @@ class Engine(QQmlApplicationEngine): yield from self._recursive_dirs_in(item) - def reload_qml(self) -> None: + def showWindow(self) -> None: + self.load(str(self.app_dir / "components" / "Window.qml")) + + + def closeWindow(self) -> None: + try: + self.rootObjects()[0].close() + except IndexError: + pass + + + def reloadQml(self) -> None: loader = self.rootObjects()[0].findChild(QObject, "UILoader") + source = loader.property("source") loader.setProperty("source", None) self.clearComponentCache() + loader.setProperty("source", source) - logging.info("Reloaded: %s", source)