Rework startup and Application-Engine relation

- Application and Engine will be started by __init__.run() independently
- Exiting app will disconnect clients
- Signals like SIGINT (Ctrl-C) are now handled for proper exit
This commit is contained in:
miruka 2019-05-01 01:23:38 -04:00
parent 5ad13aed7d
commit 12ce4cdb30
8 changed files with 72 additions and 55 deletions

View File

@ -4,8 +4,6 @@
import os import os
import sys import sys
# logging.basicConfig(level=logging.INFO)
# The disk cache is responsible for multiple display bugs when running # 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 # the app for the first time/when cache needs to be recompiled, on top
# of litering the source folders with .qmlc files. # of litering the source folders with .qmlc files.
@ -13,5 +11,11 @@ os.environ["QML_DISABLE_DISK_CACHE"] = "1"
def run() -> None: def run() -> None:
from . import app from .app import Application
_ = app.Application(sys.argv) app = Application(sys.argv)
from .engine import Engine
engine = Engine(debug=app.debug)
engine.showWindow()
sys.exit(app.exec_())

View File

@ -10,17 +10,13 @@ from . import __about__
class Application(QGuiApplication): class Application(QGuiApplication):
def __init__(self, args: Optional[List[str]] = None) -> None: def __init__(self, args: Optional[List[str]] = None) -> None:
try: self.debug = False
args.index("--debug") # type: ignore
debug = True if args and "--debug" in args:
except (AttributeError, ValueError): del args[args.index("--debug")]
debug = False self.debug = True
super().__init__(args or []) super().__init__(args or [])
self.setApplicationName(__about__.__pkg_name__) self.setApplicationName(__about__.__pkg_name__)
self.setApplicationDisplayName(__about__.__pretty_name__) self.setApplicationDisplayName(__about__.__pretty_name__)
from .engine import Engine
engine = Engine(self, debug=debug)
engine.show_window()

View File

@ -13,11 +13,6 @@ import nio
from .network_manager import NetworkManager from .network_manager import NetworkManager
from .pyqt_future import futurize 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): class Client(QObject):
roomInvited = pyqtSignal([str, dict], [str]) roomInvited = pyqtSignal([str, dict], [str])
@ -46,7 +41,7 @@ class Client(QObject):
self.host: str = host self.host: str = host
self.port: int = int(port[0]) if port else 443 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 = \ self.nio: nio.client.HttpClient = \
nio.client.HttpClient(self.host, username, device_id) nio.client.HttpClient(self.host, username, device_id)
@ -109,9 +104,12 @@ class Client(QObject):
@futurize(pyqt=False) @futurize(pyqt=False)
def startSyncing(self) -> None: def startSyncing(self) -> None:
while True: while True:
self._on_sync(self.net_sync.talk( try:
self.nio_sync.sync, timeout=8000 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(): if self._stop_sync.is_set():
self._stop_sync.clear() self._stop_sync.clear()

View File

@ -85,6 +85,13 @@ class ClientManager(QObject):
client.logout() 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) @pyqtProperty(str, constant=True)
def defaultDeviceName(self) -> str: # pylint: disable=no-self-use def defaultDeviceName(self) -> str: # pylint: disable=no-self-use
os_ = f" on {platform.system()}".rstrip() os_ = f" on {platform.system()}".rstrip()

View File

@ -80,7 +80,7 @@ def futurize(max_instances: Optional[int] = None, pyqt: bool = True
return func(self, *args, **kws) return func(self, *args, **kws)
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
logging.error("Exiting thread due to exception.") logging.error("Exiting thread/process due to exception.")
sys.exit(1) sys.exit(1)
finally: finally:
del _RUNNING[_RUNNING.index((self.pool, func, args, kws))] del _RUNNING[_RUNNING.index((self.pool, func, args, kws))]

View File

@ -4,6 +4,7 @@ import "Banners"
import "RoomEventList" import "RoomEventList"
HColumnLayout { HColumnLayout {
Component.onCompleted: Backend.pdb()
property string userId: "" property string userId: ""
property string roomId: "" property string roomId: ""

View File

@ -8,6 +8,8 @@ ApplicationWindow {
width: Math.min(Screen.width, 1152) width: Math.min(Screen.width, 1152)
height: Math.min(Screen.height, 768) height: Math.min(Screen.height, 768)
onClosing: Backend.clientManager.deleteAll()
Loader { Loader {
anchors.fill: parent anchors.fill: parent
source: "UI.qml" source: "UI.qml"

View File

@ -1,57 +1,54 @@
# Copyright 2019 miruka # Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3. # This file is part of harmonyqml, licensed under GPLv3.
import logging import signal
import sys import sys
from pathlib import Path 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 PyQt5.QtQml import QQmlApplicationEngine
from .app import Application
from .backend.backend import Backend
# logging.basicConfig(level=logging.INFO)
class Engine(QQmlApplicationEngine): class Engine(QQmlApplicationEngine):
def __init__(self, def __init__(self, debug: bool = False) -> None:
app: Application, # Connect UNXI signals to properly exit program
debug: bool = False) -> None: self._original_signal_handlers: Dict[int, Any] = {}
super().__init__(app)
self.app = app
self.backend = Backend(self)
self.app_dir = Path(sys.argv[0]).resolve().parent
# Set QML properties for signame in ("INT" , "HUP", "QUIT", "TERM"):
self.rootContext().setContextProperty("Engine", self) sig = signal.Signals[f"SIG{signame}"] # pylint: disable=no-member
self.rootContext().setContextProperty("Backend", self.backend) self._original_signal_handlers[sig] = signal.getsignal(sig)
signal.signal(sig, self.onExitSignal)
# Connect Qt signals
self.quit.connect(self.app.quit)
# Make SIGINT (ctrl-c) work # Make SIGINT (ctrl-c) work
self._sigint_timer = QTimer() self._sigint_timer = QTimer()
self._sigint_timer.timeout.connect(lambda: None) self._sigint_timer.timeout.connect(lambda: None)
self._sigint_timer.start(100) 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 # Setup UI live-reloading when a file is edited
if debug: if debug:
from PyQt5.QtCore import QFileSystemWatcher
self._watcher = 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)) self._watcher.addPath(str(self.app_dir))
for _dir in list(self._recursive_dirs_in(self.app_dir)): for _dir in list(self._recursive_dirs_in(self.app_dir)):
self._watcher.addPath(str(_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._original_signal_handlers.clear()
self.rootObjects()[0].show() self.closeWindow()
sys.exit(self.app.exec())
def _recursive_dirs_in(self, path: Path) -> Generator[Path, None, None]: 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) 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") loader = self.rootObjects()[0].findChild(QObject, "UILoader")
source = loader.property("source") source = loader.property("source")
loader.setProperty("source", None) loader.setProperty("source", None)
self.clearComponentCache() self.clearComponentCache()
loader.setProperty("source", source) loader.setProperty("source", source)
logging.info("Reloaded: %s", source)