feat: catch cancel and shutdown events

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

View File

@ -47,6 +47,7 @@ if TYPE_CHECKING:
from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.right_panel import RightPanel
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.servers import ServerNotebook
from dzgui.views.trees.tree_log import LogTreeView
@ -90,6 +91,7 @@ class Controller(GObject.GObject):
self.pending_jobs = 1
self.exit_event = threading.Event()
self.cancel_event = threading.Event()
self.connection_man: ConnectionManager
def get_emitter(self) -> Emitter:
@ -463,6 +465,15 @@ class Controller(GObject.GObject):
def set_exit_event(self) -> None:
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:
self.open_page(NotebookPage.CONNECTION)
self.mediator.preconnect.populate(prereqs)
@ -479,4 +490,3 @@ class Controller(GObject.GObject):
def get_steam_client_name(self) -> str:
return self.mediator.options.get_client_name()

View File

@ -1,27 +1,26 @@
import psutil
import subprocess
import shutil
import sys
from dzgui.const.constants import (
DAYZ_BINARY,
STEAM_CMD,
FLATPAK_APPID,
FLATPAK_CMD,
FLATPAK_RUN_CMD,
FLATPAK_SANDBOX,
)
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
from dzgui.util.strings import init
# TODO: move to util.proc
def is_dayz_running() -> bool:
"""Subprocesses spawned from Steam will not show up in regular process tree"""
procs = []
substring = DAYZ_BINARY
for proc in psutil.process_iter():
try:
procs.append(proc.cmdline())
except Exception as e:
except Exception:
continue
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")
# CHORE: test alternate clients
def is_flatpak_steam_running() -> bool:
if has_cmd(FLATPAK_CMD) is 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()
if FLATPAK_APPID in lines:
return True

View File

@ -75,7 +75,6 @@ class Prerequisites:
dayz_running: bool
steam_proc: SteamProcess
mods: list[str]
missing_mods: int
foreground_cmd: str | None
game_mode: bool
@ -184,7 +183,6 @@ class ConnectionManager:
dayz_running=dayz_running,
steam_proc=steam_proc,
mods=remote_mods,
missing_mods=len(self.missing_mods),
foreground_cmd=self.foreground_cmd, # TODO: change to bool
game_mode=game_mode,
)
@ -245,7 +243,6 @@ class ConnectionManager:
dialog = ExceptionDialog(self.controller, server_timeout)
dialog.run()
# TODO: threading
def _connect_steam(self) -> None:
addr = f"{self.record.ip}:{self.record.gameport}"
playername = self.controller.query_config(Preferences.NAME)
@ -256,8 +253,12 @@ class ConnectionManager:
self.thread_man.set_cleanup_func(func)
return
self.thread_man.show_cancel(False)
self.thread_man.update_dialog(waiting_for_launch)
while True:
if self.controller.get_exit_event().is_set():
# TODO: some facility to also close spawned steam process
return
if is_dayz_running():
break
time.sleep(1)
@ -290,6 +291,12 @@ class ConnectionManager:
while True:
# NOTE: mods will finish at the same time
# 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)
if cur_size == size:
break
@ -298,11 +305,11 @@ class ConnectionManager:
update_signatures(self.missing_mods, prefs.paths.version)
# TODO: get config path or just push steam path directly
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:
if len(self.missing_mods) > 0:
self._update_mods(raise_window)
self._connect_steam()
else:
self._connect_steam()

View File

@ -20,7 +20,9 @@ from gi.repository import Gtk, GLib # noqa E402
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:
@wraps(func)
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(
"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
@ -57,15 +59,22 @@ class ThreadingManager:
self.destroy_first = False
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:
def callback() -> None:
func.call()
GLib.idle_add(self._destroy_on_idle)
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()
thread = threading.Thread(target=callback)
thread.start()
@ -113,3 +122,6 @@ class ThreadingManager:
def get_wait_dialog(self) -> WaitDialog:
return self.wait_dialog
def show_cancel(self, state: bool) -> None:
GLib.idle_add(self.wait_dialog.show_cancel, state)

View File

@ -64,7 +64,7 @@ class ServerModCols:
@dataclass(slots=True, frozen=True)
class MenuCols:
name: str
hidden: object # GObject.TYPE_PYOBJECT
hidden: object
class FastInsertListStore(ListStore):
@ -108,6 +108,7 @@ class FastInsertListStore(ListStore):
self.append(row)
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
and may segfault if types and length are not identical to ListStore.

View File

@ -91,6 +91,7 @@ class Statusbar(Gtk.Grid):
self.set_by_context(enum, bar)
# TODO: unused
def _on_notebook_page_returned(
self, statusbar: Self, prior_context: NotebookPage
) -> None:
@ -109,8 +110,9 @@ class Statusbar(Gtk.Grid):
context: Union["ServerTab", NotebookPage],
) -> None:
self.spinner.stop()
# TODO: CalcDist is being called when table is not loaded
if dist is None:
self.set_by_context(context, self.playercount)
self.set_by_context(context, "")
else:
pretty = self.append_distance(dist)
self.set_by_context(context, pretty)

View File

@ -87,7 +87,13 @@ class NotifyDialog(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__(
controller=controller,
text=strings.wait,
@ -96,19 +102,29 @@ class WaitDialog(GenericDialog):
secondary=secondary,
)
self.controller = controller
self.jobs = jobs
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)
content = self.get_content_area()
spinner = Gtk.Spinner()
self.prog = Gtk.ProgressBar()
content.pack_end(self.cancel, NO_EXPAND, NO_FILL, 0)
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
if self.jobs > 1:
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
else:
spinner.start()
if show_cancel is False:
self.connect("realize", lambda _: self.cancel.set_visible(False))
def update_text(self, msg: str) -> None:
self.format_secondary_text(msg)
@ -119,6 +135,9 @@ class WaitDialog(GenericDialog):
self.prog.set_fraction(fraction)
self.cur_job += 1
def show_cancel(self, state: bool) -> None:
self.cancel.set_visible(state)
class QuitDialog(GenericDialog):
def __init__(self, controller: "Controller", secondary: str):

View File

@ -135,7 +135,7 @@ class ServerModDialog(ServerDialog):
columns = [
server_mods.mod,
server_mods.mod_id,
server_mods.installed,
server_mods.up_to_date,
]
for i, column_title in enumerate(columns):
renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)

View File

@ -203,8 +203,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
child.set_visible(True)
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.ok.set_sensitive(True)
self.ok.set_label(preconnect.update_mods)
@ -228,8 +226,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
warnings: list[str] = []
errors: list[str] = []
# TODO: check availability of currently selected steam cmd
"""Errors"""
if prereqs.binary_missing:
errors.append(
@ -273,6 +269,17 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.error_placeholder.set_visible(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:
mods = prereqs.mods
self.tree.populate(mods)
@ -281,27 +288,23 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
name = prereqs.name
self.title.set_text(name)
if prereqs.foreground_cmd is not None:
self.raise_window.set_sensitive(True)
suffix = "All mods are up to date."
if prereqs.missing_mods < 1:
self.ok.set_label(preconnect.connect)
if total_mods < 1:
self.scrolled.set_visible(False)
self.progress_box.set_visible(False)
self.mods_placeholder.set_visible(True)
self._hide_mod_area()
else:
self.scrolled.set_visible(True)
self.progress_box.set_visible(True)
self.raise_window.set_visible(True)
self.mods_placeholder.set_visible(False)
self.tree.grab_focus()
self._show_mod_area()
# TODO: strings
suffix = "All mods are up to date."
if prereqs.required_space > 0:
pretty = number(prereqs.required_space)
suffix = f" Need to download {pretty} MiB of mod updates."
if prereqs.required_space == 0:
self.ok.set_label(preconnect.connect)
self.raise_window.set_visible(False)
else:
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
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")