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 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_())

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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))]

View File

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

View File

@ -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"

View File

@ -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)