feat: catch cancel and shutdown events

This commit is contained in:
aclist committed 2026-05-11 00:52:48 +09:00
1 parent 319966f8e1
commit 2e5860650f
9 files changed
+94 -40

No files matched your search

+11 -1
View File
@@ -47,6 +47,7 @@ if TYPE_CHECKING:
from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.right_panel import RightPanel from dzgui.views.components.right_panel import RightPanel
from dzgui.views.components.statusbar import Statusbar from dzgui.views.components.statusbar import Statusbar
from dzgui.views.pages.options import Options
from dzgui.views.pages.preconnect import PreConnectionAssistant from dzgui.views.pages.preconnect import PreConnectionAssistant
from dzgui.views.pages.servers import ServerNotebook from dzgui.views.pages.servers import ServerNotebook
from dzgui.views.trees.tree_log import LogTreeView from dzgui.views.trees.tree_log import LogTreeView
@@ -90,6 +91,7 @@ class Controller(GObject.GObject):
self.pending_jobs = 1 self.pending_jobs = 1
self.exit_event = threading.Event() self.exit_event = threading.Event()
self.cancel_event = threading.Event()
self.connection_man: ConnectionManager self.connection_man: ConnectionManager
def get_emitter(self) -> Emitter: def get_emitter(self) -> Emitter:
@@ -463,6 +465,15 @@ class Controller(GObject.GObject):
def set_exit_event(self) -> None: def set_exit_event(self) -> None:
self.exit_event.set() self.exit_event.set()
def set_cancel_event(self) -> None:
self.cancel_event.set()
def get_cancel_event(self) -> threading.Event:
return self.cancel_event
def clear_cancel_event(self) -> None:
self.cancel_event.clear()
def open_connection_assistant(self, prereqs: "Prerequisites") -> None: def open_connection_assistant(self, prereqs: "Prerequisites") -> None:
self.open_page(NotebookPage.CONNECTION) self.open_page(NotebookPage.CONNECTION)
self.mediator.preconnect.populate(prereqs) self.mediator.preconnect.populate(prereqs)
@@ -479,4 +490,3 @@ class Controller(GObject.GObject):
def get_steam_client_name(self) -> str: def get_steam_client_name(self) -> str:
return self.mediator.options.get_client_name() return self.mediator.options.get_client_name()
+5 -5
View File
@@ -1,27 +1,26 @@
import psutil import psutil
import subprocess import subprocess
import shutil import shutil
import sys
from dzgui.const.constants import ( from dzgui.const.constants import (
DAYZ_BINARY, DAYZ_BINARY,
STEAM_CMD, STEAM_CMD,
FLATPAK_APPID, FLATPAK_APPID,
FLATPAK_CMD,
FLATPAK_RUN_CMD, FLATPAK_RUN_CMD,
FLATPAK_SANDBOX, FLATPAK_SANDBOX,
) )
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
from dzgui.util.strings import init
# TODO: move to util.proc # TODO: move to util.proc
def is_dayz_running() -> bool: def is_dayz_running() -> bool:
"""Subprocesses spawned from Steam will not show up in regular process tree"""
procs = [] procs = []
substring = DAYZ_BINARY substring = DAYZ_BINARY
for proc in psutil.process_iter(): for proc in psutil.process_iter():
try: try:
procs.append(proc.cmdline()) procs.append(proc.cmdline())
except Exception as e: except Exception:
continue continue
return any(substring in item for sublist in procs for item in sublist) return any(substring in item for sublist in procs for item in sublist)
@@ -37,10 +36,11 @@ def is_steam_running(cmd: str) -> bool:
raise TypeError("Not a valid Steam client selection") raise TypeError("Not a valid Steam client selection")
# CHORE: test alternate clients
def is_flatpak_steam_running() -> bool: def is_flatpak_steam_running() -> bool:
if has_cmd(FLATPAK_CMD) is False: if has_cmd(FLATPAK_CMD) is False:
return False return False
proc = subprocess.check_output([FLATPAK_CMD, "ps"], capture_output=True, text=True) proc = subprocess.check_output([FLATPAK_CMD, "ps"], text=True)
lines = proc.stdout.splitlines() lines = proc.stdout.splitlines()
if FLATPAK_APPID in lines: if FLATPAK_APPID in lines:
return True return True
+12 -5
View File
@@ -75,7 +75,6 @@ class Prerequisites:
dayz_running: bool dayz_running: bool
steam_proc: SteamProcess steam_proc: SteamProcess
mods: list[str] mods: list[str]
missing_mods: int
foreground_cmd: str | None foreground_cmd: str | None
game_mode: bool game_mode: bool
@@ -184,7 +183,6 @@ class ConnectionManager:
dayz_running=dayz_running, dayz_running=dayz_running,
steam_proc=steam_proc, steam_proc=steam_proc,
mods=remote_mods, mods=remote_mods,
missing_mods=len(self.missing_mods),
foreground_cmd=self.foreground_cmd, # TODO: change to bool foreground_cmd=self.foreground_cmd, # TODO: change to bool
game_mode=game_mode, game_mode=game_mode,
) )
@@ -245,7 +243,6 @@ class ConnectionManager:
dialog = ExceptionDialog(self.controller, server_timeout) dialog = ExceptionDialog(self.controller, server_timeout)
dialog.run() dialog.run()
# TODO: threading
def _connect_steam(self) -> None: def _connect_steam(self) -> None:
addr = f"{self.record.ip}:{self.record.gameport}" addr = f"{self.record.ip}:{self.record.gameport}"
playername = self.controller.query_config(Preferences.NAME) playername = self.controller.query_config(Preferences.NAME)
@@ -256,8 +253,12 @@ class ConnectionManager:
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
return return
self.thread_man.show_cancel(False)
self.thread_man.update_dialog(waiting_for_launch) self.thread_man.update_dialog(waiting_for_launch)
while True: while True:
if self.controller.get_exit_event().is_set():
# TODO: some facility to also close spawned steam process
return
if is_dayz_running(): if is_dayz_running():
break break
time.sleep(1) time.sleep(1)
@@ -290,6 +291,12 @@ class ConnectionManager:
while True: while True:
# NOTE: mods will finish at the same time # NOTE: mods will finish at the same time
# TODO: check for early cancel event # TODO: check for early cancel event
# TODO: check global sigint event
if self.controller.get_exit_event().is_set():
return
if self.controller.get_cancel_event().is_set():
self.controller.clear_cancel_event()
return
cur_size = get_mod_dir_size(mod_path) cur_size = get_mod_dir_size(mod_path)
if cur_size == size: if cur_size == size:
break break
@@ -298,11 +305,11 @@ class ConnectionManager:
update_signatures(self.missing_mods, prefs.paths.version) update_signatures(self.missing_mods, prefs.paths.version)
# TODO: get config path or just push steam path directly # TODO: get config path or just push steam path directly
rebuild_symlinks(prefs.paths.config) rebuild_symlinks(prefs.paths.config)
self._connect_steam()
@call_on_thread("Waiting for Steam to update mods") @call_on_thread("Waiting for Steam to update mods", show_cancel=True)
def update_and_connect(self, raise_window: bool) -> None: def update_and_connect(self, raise_window: bool) -> None:
if len(self.missing_mods) > 0: if len(self.missing_mods) > 0:
self._update_mods(raise_window) self._update_mods(raise_window)
self._connect_steam()
else: else:
self._connect_steam() self._connect_steam()
+16 -4
View File
@@ -20,7 +20,9 @@ from gi.repository import Gtk, GLib # noqa E402
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
def call_on_thread(dialog_str: str, show_dialog: bool = True) -> Callable: def call_on_thread(
dialog_str: str, show_dialog: bool = True, show_cancel: bool = False
) -> Callable:
def decorator(func: Callable) -> Callable: def decorator(func: Callable) -> Callable:
@wraps(func) @wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> None: def wrapper(*args: Any, **kwargs: Any) -> None:
@@ -32,7 +34,7 @@ def call_on_thread(dialog_str: str, show_dialog: bool = True) -> Callable:
raise TypeError( raise TypeError(
"Attribute 'thread_man' must be of type 'ThreadingManager'" "Attribute 'thread_man' must be of type 'ThreadingManager'"
) )
self.thread_man.call_on_thread(dialog_str, stored, show_dialog) self.thread_man.call_on_thread(dialog_str, stored, show_dialog, show_cancel)
return wrapper return wrapper
@@ -57,15 +59,22 @@ class ThreadingManager:
self.destroy_first = False self.destroy_first = False
def call_on_thread( def call_on_thread(
self, dialog_str: str, func: StoredFunc, show_dialog: bool = True self,
dialog_str: str,
func: StoredFunc,
show_dialog: bool = True,
show_cancel: bool = False,
) -> None: ) -> None:
def callback() -> None: def callback() -> None:
func.call() func.call()
GLib.idle_add(self._destroy_on_idle) GLib.idle_add(self._destroy_on_idle)
if show_dialog: if show_dialog:
self.wait_dialog = WaitDialog(self.controller, dialog_str, jobs=self.jobs) self.wait_dialog = WaitDialog(
self.controller, dialog_str, jobs=self.jobs, show_cancel=show_cancel
)
self.wait_dialog.show_all() self.wait_dialog.show_all()
thread = threading.Thread(target=callback) thread = threading.Thread(target=callback)
thread.start() thread.start()
@@ -113,3 +122,6 @@ class ThreadingManager:
def get_wait_dialog(self) -> WaitDialog: def get_wait_dialog(self) -> WaitDialog:
return self.wait_dialog return self.wait_dialog
def show_cancel(self, state: bool) -> None:
GLib.idle_add(self.wait_dialog.show_cancel, state)
+2 -1
View File
@@ -64,7 +64,7 @@ class ServerModCols:
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class MenuCols: class MenuCols:
name: str name: str
hidden: object # GObject.TYPE_PYOBJECT hidden: object
class FastInsertListStore(ListStore): class FastInsertListStore(ListStore):
@@ -108,6 +108,7 @@ class FastInsertListStore(ListStore):
self.append(row) self.append(row)
def append(self, row: list[Any] | tuple[Any, ...] | None = ...) -> TreeIter: def append(self, row: list[Any] | tuple[Any, ...] | None = ...) -> TreeIter:
# FIXME: argument cannot be none
""" """
Optimized for speed, but makes no assurances about row homogeneity Optimized for speed, but makes no assurances about row homogeneity
and may segfault if types and length are not identical to ListStore. and may segfault if types and length are not identical to ListStore.
+3 -1
View File
@@ -91,6 +91,7 @@ class Statusbar(Gtk.Grid):
self.set_by_context(enum, bar) self.set_by_context(enum, bar)
# TODO: unused
def _on_notebook_page_returned( def _on_notebook_page_returned(
self, statusbar: Self, prior_context: NotebookPage self, statusbar: Self, prior_context: NotebookPage
) -> None: ) -> None:
@@ -109,8 +110,9 @@ class Statusbar(Gtk.Grid):
context: Union["ServerTab", NotebookPage], context: Union["ServerTab", NotebookPage],
) -> None: ) -> None:
self.spinner.stop() self.spinner.stop()
# TODO: CalcDist is being called when table is not loaded
if dist is None: if dist is None:
self.set_by_context(context, self.playercount) self.set_by_context(context, "")
else: else:
pretty = self.append_distance(dist) pretty = self.append_distance(dist)
self.set_by_context(context, pretty) self.set_by_context(context, pretty)
+20 -1
View File
@@ -87,7 +87,13 @@ class NotifyDialog(GenericDialog):
class WaitDialog(GenericDialog): class WaitDialog(GenericDialog):
def __init__(self, controller: "Controller", secondary: str, jobs: int = 1): def __init__(
self,
controller: "Controller",
secondary: str,
jobs: int = 1,
show_cancel: bool = False,
):
super().__init__( super().__init__(
controller=controller, controller=controller,
text=strings.wait, text=strings.wait,
@@ -96,19 +102,29 @@ class WaitDialog(GenericDialog):
secondary=secondary, secondary=secondary,
) )
self.controller = controller
self.jobs = jobs self.jobs = jobs
self.cur_job = 1 self.cur_job = 1
self.cancel = Gtk.Button(label="Cancel", halign=Gtk.Align.CENTER)
self.cancel.connect("clicked", lambda _: self.controller.set_cancel_event())
self.connect("delete-event", lambda widget, event: True) self.connect("delete-event", lambda widget, event: True)
content = self.get_content_area() content = self.get_content_area()
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
self.prog = Gtk.ProgressBar() self.prog = Gtk.ProgressBar()
content.pack_end(self.cancel, NO_EXPAND, NO_FILL, 0)
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0) content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
if self.jobs > 1: if self.jobs > 1:
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0) content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
else: else:
spinner.start() spinner.start()
if show_cancel is False:
self.connect("realize", lambda _: self.cancel.set_visible(False))
def update_text(self, msg: str) -> None: def update_text(self, msg: str) -> None:
self.format_secondary_text(msg) self.format_secondary_text(msg)
@@ -119,6 +135,9 @@ class WaitDialog(GenericDialog):
self.prog.set_fraction(fraction) self.prog.set_fraction(fraction)
self.cur_job += 1 self.cur_job += 1
def show_cancel(self, state: bool) -> None:
self.cancel.set_visible(state)
class QuitDialog(GenericDialog): class QuitDialog(GenericDialog):
def __init__(self, controller: "Controller", secondary: str): def __init__(self, controller: "Controller", secondary: str):
+1 -1
View File
@@ -135,7 +135,7 @@ class ServerModDialog(ServerDialog):
columns = [ columns = [
server_mods.mod, server_mods.mod,
server_mods.mod_id, server_mods.mod_id,
server_mods.installed, server_mods.up_to_date,
] ]
for i, column_title in enumerate(columns): for i, column_title in enumerate(columns):
renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END) renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
+24 -21
View File
@@ -203,8 +203,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
child.set_visible(True) child.set_visible(True)
self.raise_window.set_visible(False) self.raise_window.set_visible(False)
# TODO: enable button if wmctrl or xdotool is available
# TODO: check this at boot time and pass in via connection manager
self.raise_window.set_sensitive(False) self.raise_window.set_sensitive(False)
self.ok.set_sensitive(True) self.ok.set_sensitive(True)
self.ok.set_label(preconnect.update_mods) self.ok.set_label(preconnect.update_mods)
@@ -228,8 +226,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
warnings: list[str] = [] warnings: list[str] = []
errors: list[str] = [] errors: list[str] = []
# TODO: check availability of currently selected steam cmd
"""Errors""" """Errors"""
if prereqs.binary_missing: if prereqs.binary_missing:
errors.append( errors.append(
@@ -273,6 +269,17 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.error_placeholder.set_visible(False) self.error_placeholder.set_visible(False)
self.ok.set_sensitive(False) self.ok.set_sensitive(False)
def _hide_mod_area(self) -> None:
self.scrolled.set_visible(False)
self.progress_box.set_visible(False)
self.mods_placeholder.set_visible(True)
def _show_mod_area(self) -> None:
self.scrolled.set_visible(True)
self.progress_box.set_visible(True)
self.mods_placeholder.set_visible(False)
self.tree.grab_focus()
def populate(self, prereqs: "Prerequisites") -> None: def populate(self, prereqs: "Prerequisites") -> None:
mods = prereqs.mods mods = prereqs.mods
self.tree.populate(mods) self.tree.populate(mods)
@@ -281,27 +288,23 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
name = prereqs.name name = prereqs.name
self.title.set_text(name) self.title.set_text(name)
if prereqs.foreground_cmd is not None: suffix = "All mods are up to date."
self.raise_window.set_sensitive(True)
if prereqs.missing_mods < 1:
self.ok.set_label(preconnect.connect)
if total_mods < 1: if total_mods < 1:
self.scrolled.set_visible(False) self._hide_mod_area()
self.progress_box.set_visible(False)
self.mods_placeholder.set_visible(True)
else: else:
self.scrolled.set_visible(True) self._show_mod_area()
self.progress_box.set_visible(True)
self.raise_window.set_visible(True)
self.mods_placeholder.set_visible(False)
self.tree.grab_focus()
# TODO: strings if prereqs.required_space == 0:
suffix = "All mods are up to date." self.ok.set_label(preconnect.connect)
if prereqs.required_space > 0: self.raise_window.set_visible(False)
pretty = number(prereqs.required_space) else:
suffix = f" Need to download {pretty} MiB of mod updates." self.raise_window.set_visible(True)
if prereqs.foreground_cmd is not None:
self.raise_window.set_sensitive(True)
pretty = number(prereqs.required_space)
suffix = f" Need to download {pretty} MiB of mod updates."
prefix = preconnect.total_mods prefix = preconnect.total_mods
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}") self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")