feat: add ConnectionManager

This commit is contained in:
aclist committed 2026-02-23 00:29:52 +09:00
1 parent e56b89fd64
commit 6bbe0d6882
9 files changed
+159 -101

No files matched your search

+10 -5
View File
@@ -1,14 +1,18 @@
import logging import logging
import requests import requests
from pathlib import Path from pathlib import Path
from typing import Optional, TYPE_CHECKING
from dzgui.api.servers import Record
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.const.endpoints import BM_SERVERS from dzgui.const.endpoints import BM_SERVERS
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from dzgui.api.servers import Record
def get_attributes(config: Path, uid: int) -> str: def get_attributes(config: Path, uid: int) -> str:
# TODO: handle if key is not set # TODO: handle if key is not set
# TODO: tests for malformed IDs/values # TODO: tests for malformed IDs/values
@@ -18,7 +22,7 @@ def get_attributes(config: Path, uid: int) -> str:
payload: dict[str, str] = { payload: dict[str, str] = {
"filter[game]": "dayz", "filter[game]": "dayz",
"sort": "-players", "sort": "-players",
"filter[ids][whitelist]": str(uid) "filter[ids][whitelist]": str(uid),
} }
res = requests.get(BM_SERVERS, params=payload, headers=hdr) res = requests.get(BM_SERVERS, params=payload, headers=hdr)
res.raise_for_status() res.raise_for_status()
@@ -26,13 +30,14 @@ def get_attributes(config: Path, uid: int) -> str:
return j return j
def map_id_to_record(config: Path, uid: int) -> Record | None: def map_id_to_record(config: Path, uid: int) -> Optional["Record"]:
from dzgui.api.servers import Record
try: try:
record = get_attributes(config, uid) record = get_attributes(config, uid)
ip = record["ip"] ip = record["ip"]
port = record["port"] port = record["port"]
qport = record["portQuery"] qport = record["portQuery"]
return Record(ip, port, qport) return Record(ip, port, qport)
except Exception as e: except Exception:
logger.warn(e)
return None return None
+25
View File
@@ -12,6 +12,7 @@ import typing # noqa
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union from typing import Union
from dzgui.api.bm import map_id_to_record
from dzgui.const.constants import REQUEST_TIMEOUT from dzgui.const.constants import REQUEST_TIMEOUT
from dzgui.const.endpoints import STEAM_SERVERS from dzgui.const.endpoints import STEAM_SERVERS
from dzgui.util import strings from dzgui.util import strings
@@ -451,3 +452,27 @@ def get_rules(ip: str, qport: int) -> list[int]:
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
return [] return []
def response_to_fq_ip(res: dict) -> str:
ip = res["addr"].split(":")[0]
gameport = res["gameport"]
qport = res["addr"].split(":")[1]
return f"{ip}:{gameport}:{qport}"
def query_id_or_ip(addr: str) -> None:
# NOTE: Battlemetrics
if addr.isdigit():
try:
config = self.controller.get_prefs().paths.config
resolved = map_id_to_record(config, addr)
res = query_direct(resolved.ip, resolved.qport)
except Exception as e:
logger.critical(e)
return None
else:
record = addr.split(":")
ip, qport = record[0], record[1]
res = query_direct(ip, int(qport))
return res
+17 -10
View File
@@ -113,8 +113,8 @@ class Controller(GObject.GObject):
self.emitter = Emitter() self.emitter = Emitter()
self.emitter.connect("map_selection_changed", self._on_map_selection_changed) self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
self.emitter.connect("check_toggled", self._on_check_toggled) self.emitter.connect("check_toggled", self._on_check_toggled)
self.emitter.connect("servers_loaded", self._on_servers_loaded)
self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init) self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init)
#self.emitter.connect("servers_loaded", self._on_servers_loaded)
# NOTE: suppress requests until entire UI is loaded # NOTE: suppress requests until entire UI is loaded
self.loaded = False self.loaded = False
@@ -861,7 +861,11 @@ class Controller(GObject.GObject):
self.filter_man = filter_man self.filter_man = filter_man
def populate_model(self, tv: Gtk.TreeView) -> None: def populate_model(self, tv: Gtk.TreeView) -> None:
ServerModelManager(self, tv, first_iteration=True) # NOTE: skip on previously loaded tabs
if tv.is_loaded():
self.emitter.emit("servers_loaded", tv.get_enum())
return
ServerModelManager(self, tv, first_iteration=True).load()
def get_favorite(self) -> tuple[str, str] | tuple[None, None]: def get_favorite(self) -> tuple[str, str] | tuple[None, None]:
fav = str(self.query_config(Preferences.FAV_LBL)) fav = str(self.query_config(Preferences.FAV_LBL))
@@ -913,14 +917,14 @@ class Controller(GObject.GObject):
store = self.get_map_store() store = self.get_map_store()
self.emitter.emit("load_maps", store) self.emitter.emit("load_maps", store)
def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None: #def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None:
return # return
# NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None # # NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None
# TODO: this should be internal to servers page # # TODO: this should be internal to servers page
state = self.has_server_model() # state = self.has_server_model()
tv = self.get_active_treeview() # tv = self.get_active_treeview()
tv.set_headers_visible(state) # tv.set_headers_visible(state)
tv.set_headers_clickable(state) # tv.set_headers_clickable(state)
def has_server_model(self) -> bool: def has_server_model(self) -> bool:
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
@@ -946,6 +950,9 @@ class Controller(GObject.GObject):
return self.mediator.notebook return self.mediator.notebook
def get_servers(self) -> "Notebook": def get_servers(self) -> "Notebook":
return self.mediator.servers
def get_server_notebook(self) -> "Notebook":
return self.mediator.servers.notebook return self.mediator.servers.notebook
def get_window(self) -> "OuterWindow": def get_window(self) -> "OuterWindow":
+35
View File
@@ -0,0 +1,35 @@
from typing import TYPE_CHECKING
import dzgui.api.servers as Servers
from dzgui.managers.thread_man import call_on_thread, StoredFunc, ThreadingManager
from dzgui.util.strings import api_warn_msg, dialog
from dzgui.views.dialogs.generic import ExceptionDialog
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
class ConnectionManager:
def __init__(self, controller: "Controller") -> None:
self.controller = controller
self.thread_man = ThreadingManager(parent=controller)
@call_on_thread(dialog.querying)
def _connect_by_id_or_ip(self, addr: str) -> None:
res = Servers.query_id_or_ip(addr)
if res is None:
self.thread_man.set_cleanup_func(StoredFunc(self._connection_failure))
# TODO: add to history if successful
print(res)
def _connection_failure(self) -> None:
# TODO: more explicit warning message, not necessarily API failure?
dialog = ExceptionDialog(self.controller, api_warn_msg)
dialog.run()
-1
View File
@@ -21,7 +21,6 @@ def call_on_thread(dialog_str: str) -> Callable:
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
self = args[0] self = args[0]
stored = StoredFunc(func, *args, **kwargs) stored = StoredFunc(func, *args, **kwargs)
print("SELF", self)
if not hasattr(self, "thread_man"): if not hasattr(self, "thread_man"):
raise AttributeError raise AttributeError
if type(self.thread_man) is not ThreadingManager: if type(self.thread_man) is not ThreadingManager:
+62 -80
View File
@@ -1,40 +1,36 @@
from datetime import datetime
import logging import logging
import threading import threading
from concurrent.futures import as_completed
from concurrent.futures import wait, as_completed
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from typing import Optional, TYPE_CHECKING
import dzgui.api.servers as Servers import dzgui.api.servers as Servers
from dzgui.const.enum import FilterMode, Preferences, ServerTab
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
) )
from dzgui.const.enum import FilterMode, Preferences, ServerTab
from dzgui.managers.thread_man import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.thread_man import call_on_thread, StoredFunc, ThreadingManager
from dzgui.util.strings import api_warn_msg, dialog from dzgui.util.strings import api_warn_msg, dialog
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
from typing import Optional, TYPE_CHECKING
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib # noqa E402 from gi.repository import Gtk # noqa E402
LAN_TIMEOUT = 0.5 LAN_TIMEOUT = 0.5
API_TIMEOUT = 3 API_TIMEOUT = 3
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
from dzgui.model.filtered_model import FilteredModelManager from dzgui.model.filtered_model import FilteredModelManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# TODO: failure: spawns error dialog # TODO: failure: spawns error dialog
# TODO: non failure with empty model: updates statusbar # TODO: non failure with empty model: updates statusbar with help text
class ServerModelManager: class ServerModelManager:
@@ -42,28 +38,32 @@ class ServerModelManager:
self, controller: "Controller", tv: Gtk.TreeView, first_iteration=False self, controller: "Controller", tv: Gtk.TreeView, first_iteration=False
) -> None: ) -> None:
enum = tv.get_enum() self.tv = tv
self.enum = tv.get_enum()
self.controller = controller self.controller = controller
self.emitter = controller.get_emitter() self.emitter = controller.get_emitter()
if tv.is_loaded():
self.emitter.emit("servers_loaded", enum)
return
self.tv = tv
self.jobs = 1 self.jobs = 1
# NOTE: store filter man for access inside thread
self.filter_man = tv.get_filter_man() self.filter_man = tv.get_filter_man()
# FIXME: change WaitDialog to use parent window only
self.thread_man = ThreadingManager(parent=controller) self.thread_man = ThreadingManager(parent=controller)
# TODO: if first iteration, clear filter man control model # TODO: if first iteration, clear filter man control model
# literal first load: iteration 1 # literal first load: iteration 1
# refresh: iteration 1 (wipe model) # refresh: iteration 1 (wipe model)
# filter: iteration N+1 # filter: iteration N+1
# TODO: can drop first iteration arg and process in methods
self.first_iteration = first_iteration self.first_iteration = first_iteration
# TODO: pass servers.saved treeview when using conpan regardless of current context def load(self) -> None:
"""
match enum: There may be cases where you want to instantiate this class without dumping servers,
e.g., adding saved servers from another tab
"""
match self.enum:
case ServerTab.BROWSER: case ServerTab.BROWSER:
# NOTE: extra DAYZ_EXP param # NOTE: extra DAYZ_EXP param
self.thread_man.set_job_count(len(Servers.params) + 1) self.thread_man.set_job_count(len(Servers.params) + 1)
@@ -71,14 +71,12 @@ class ServerModelManager:
case ServerTab.SAVED: case ServerTab.SAVED:
self._dump_favorites() self._dump_favorites()
case ServerTab.RECENT: case ServerTab.RECENT:
# TODO: get row count
self._dump_history() self._dump_history()
case ServerTab.LAN: case ServerTab.LAN:
# NOTE: LAN tab is only loaded on demand # NOTE: LAN tab is only loaded on demand
pass pass
case _: case _:
pass pass
# self._dump_lan()
@call_on_thread(dialog.fetching) @call_on_thread(dialog.fetching)
def _dump_api(self) -> None: def _dump_api(self) -> None:
@@ -113,7 +111,6 @@ class ServerModelManager:
j = res.json j = res.json
servers += j["response"]["servers"] servers += j["response"]["servers"]
# TODO: try/except when parsing
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data(parsed, FilterMode.INITIAL) self._push_data(parsed, FilterMode.INITIAL)
@@ -150,11 +147,10 @@ class ServerModelManager:
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data(parsed, FilterMode.INITIAL) self._push_data(parsed, FilterMode.INITIAL)
# TODO: strings @call_on_thread(dialog.fetching)
@call_on_thread("dumping ips")
def _dump_ips(self, ips: list[str]) -> None: def _dump_ips(self, ips: list[str]) -> None:
# NOTE: block malformed records (TODO: add github issue no.) # NOTE: block malformed records (TODO: add github issue no.)
# TODO: sanitize ip list at config time, drop this # TODO: sanitize ip list at config time and drop this
ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""] ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""]
job = Servers.query_direct job = Servers.query_direct
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
@@ -183,42 +179,32 @@ class ServerModelManager:
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data(parsed, FilterMode.INITIAL) self._push_data(parsed, FilterMode.INITIAL)
def _query_ip_id(self, addr: str) -> None: @call_on_thread(dialog.querying)
# NOTE: Battlemetrics
if addr.isdigit():
# FIXME:
config = self.controller.get_prefs().paths.config
resolved = map_id_to_record(config, addr)
res = Servers.query_direct(resolved.ip, resolved.qport)
else:
record = addr.split(":")
ip, qport = record[0], record[1]
res = Servers.query_direct(ip, int(qport))
return res
# TODO: strings
@call_on_thread("querying address")
def _connect_by_id_or_ip(self, addr: str) -> None:
res = self._query_ip_id(addr)
# TODO: unimplemented
@call_on_thread("querying address")
def _add_by_id_or_ip(self, addr: str) -> None: def _add_by_id_or_ip(self, addr: str) -> None:
res = self.query_ip_id(addr) res = Servers.query_id_or_ip(addr)
# TODO: investigate this
if res is None: if res is None:
self.set_cleanup_func(StoredFunc(self._cleanup_on_failure)) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure))
return return
# NOTE: single record insertion
self.insert_record = Servers.parse_json([res]) record = Servers.parse_json([res])
# TODO: add into saved servers file
# TODO: update saved servers model filter_man = self._get_filter_man()
# NOTE: this can be called from other tabs--if current focus is not ServerTab.SAVED, update label only model = filter_man.get_control()
# TODO: saved servers might not be loaded yet, in which case should just update local file only
# TODO: perform simple equality comparison of self.tv.get_enum() == ServerTab.SAVED # TODO: make this a method of ConfigManager
# FIXME: filter man is saved on a per tab basis, so this will mismatch fqip = Servers.response_to_fq_ip(res)
# check if servers.get_active_treeview() is same as self.tv ips = self.controller.query_config(Preferences.IP_LIST)
self.set_cleanup_func(StoredFunc(self._cleanup_on_insert)) ips.append(fqip)
self.controller.update_config(Preferences.IP_LIST, ips)
if model is not None:
# NOTE: single record insertion
model.append(record[0])
# TODO: if all filters are already applied, strange behavior may occur
# -> need to insert and update per current filters
filter_man.filter(FilterMode.INITIAL)
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_single_ip))
def _dump_history(self) -> None: def _dump_history(self) -> None:
history = self.controller.get_prefs().paths.history history = self.controller.get_prefs().paths.history
@@ -242,6 +228,7 @@ class ServerModelManager:
def _dump_favorites(self) -> None: def _dump_favorites(self) -> None:
ips = self.controller.query_config(Preferences.IP_LIST) ips = self.controller.query_config(Preferences.IP_LIST)
self.thread_man.set_job_count(len(ips)) self.thread_man.set_job_count(len(ips))
# TODO: customize statusbar to mention how records can be added via contextmenu # TODO: customize statusbar to mention how records can be added via contextmenu
if len(ips) == 0: if len(ips) == 0:
# FIXME: this is not a failure, just a quiet exit with custom statusbar # FIXME: this is not a failure, just a quiet exit with custom statusbar
@@ -252,25 +239,20 @@ class ServerModelManager:
return return
self._dump_ips(ips) self._dump_ips(ips)
def _cleanup_on_insert(self) -> None: def _cleanup_single_ip(self) -> None:
filter_man = self.get_filter_man() proxy = self._get_filter_man().get_proxy_model()
model = filter_man.get_control()
# FIXME: this is a single row insertion,
# but refiltration should occur in thread for consistency/scalability
model.append(self.insert_record[0])
proxy = filter_man.filter(FilterMode.INITIAL)
# TODO: get proxy model out of thread
proxy = self.get_filter_man().get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
# TODO: update statusbar # TODO: if current tab != self.saved, add label
context = self.tv.get_enum()
# TODO: adding a row may update available maps # TODO: adding a row may update available maps
# TODO: if all filters are already applied, strange behavior may occur self.emitter.emit("servers_loaded", self.enum)
# -> need to insert and update per current filters self._update_maps()
self.emitter.emit("servers_loaded", context)
def _update_maps(self) -> None:
map_man = self.tv.get_map_man()
map_man.set_unique_maps(self._get_new_maps())
self.emitter.emit("servers_loaded_init")
self.first_iteration = False
def _cleanup_on_success(self) -> None: def _cleanup_on_success(self) -> None:
self.tv.set_model(self.to_insert) self.tv.set_model(self.to_insert)
@@ -285,14 +267,8 @@ class ServerModelManager:
context = self.tv.get_enum() context = self.tv.get_enum()
self.emitter.emit("servers_loaded", context) self.emitter.emit("servers_loaded", context)
# CHORE: this is placeholder logic,
# refills map combo with new maps
if self.first_iteration: if self.first_iteration:
map_man = self.tv.get_map_man() self._update_maps()
map_man.set_unique_maps(self.new_maps)
self.emitter.emit("servers_loaded_init")
self.first_iteration = False
self.new_maps = None
def _cleanup_on_failure(self, show_dialog=True) -> None: def _cleanup_on_failure(self, show_dialog=True) -> None:
map_man = self.tv.get_map_man() map_man = self.tv.get_map_man()
@@ -334,10 +310,16 @@ class ServerModelManager:
self.to_insert = manager.get_proxy_model() self.to_insert = manager.get_proxy_model()
u_maps = set([row[1] for row in data]) u_maps = set([row[1] for row in data])
self.new_maps = sorted(u_maps) self._set_new_maps(sorted(u_maps))
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success)) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
def _set_new_maps(self, maps: list[str]) -> None:
self.new_maps = maps
def _get_new_maps(self) -> list[str]:
return self.new_maps
def _get_filter_man(self) -> "FilteredModelManager": def _get_filter_man(self) -> "FilteredModelManager":
return self.filter_man return self.filter_man
+8 -3
View File
@@ -1,5 +1,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.managers.connection import ConnectionManager
from dzgui.model.servers import ServerModelManager from dzgui.model.servers import ServerModelManager
from dzgui.util.strings import connect_panel, fav_panel, lan_panel from dzgui.util.strings import connect_panel, fav_panel, lan_panel
from dzgui.views.components.buttons import ( from dzgui.views.components.buttons import (
@@ -178,6 +179,7 @@ class AddPanel(Gtk.Frame):
self.add_server.connect("clicked", self._on_add_clicked) self.add_server.connect("clicked", self._on_add_clicked)
self.conn_server = SteamConnectButton() self.conn_server = SteamConnectButton()
self.conn_server.connect("clicked", self._on_connect_clicked)
self.conn_server.set_sensitive(False) self.conn_server.set_sensitive(False)
self.add_server.set_sensitive(False) self.add_server.set_sensitive(False)
@@ -199,11 +201,14 @@ class AddPanel(Gtk.Frame):
self.add(self.grid) self.add(self.grid)
def _on_connect_clicked(self, button: Gtk.Button) -> None:
text = self.entry.get_text()
ConnectionManager(self.controller)._connect_by_id_or_ip(text)
def _submit_query(self) -> None: def _submit_query(self) -> None:
text = self.entry.get_text() text = self.entry.get_text()
# TODO: call servermodelmanager saved = self.controller.get_servers().get_saved()
ServerModelManager().add_by_id_or_ip(text) ServerModelManager(self.controller, saved)._add_by_id_or_ip(text)
# self.controller.add_by_id_or_ip(text)
def _on_activate(self, entry: Gtk.Entry) -> None: def _on_activate(self, entry: Gtk.Entry) -> None:
if not self.add_server.is_sensitive(): if not self.add_server.is_sensitive():
+1 -1
View File
@@ -22,7 +22,7 @@ class Breadcrumbs(Gtk.Label):
self.set_markup(crumbs) self.set_markup(crumbs)
notebook = controller.get_notebook() notebook = controller.get_notebook()
servers = controller.get_servers() servers = controller.get_server_notebook()
notebook.connect_after("switch-page", self._on_notebook_page_changed) notebook.connect_after("switch-page", self._on_notebook_page_changed)
servers.connect_after("switch-page", self._on_server_tab_changed) servers.connect_after("switch-page", self._on_server_tab_changed)
+1 -1
View File
@@ -2,7 +2,6 @@ import logging
from typing import Optional, Self, TYPE_CHECKING from typing import Optional, Self, TYPE_CHECKING
from dzgui.controllers.mc import StoredFunc
from dzgui.const.enum import ContextMenuGroup, ServerTab from dzgui.const.enum import ContextMenuGroup, ServerTab
from dzgui.views.trees.tree_servers import ServerTreeView from dzgui.views.trees.tree_servers import ServerTreeView
from dzgui.util.strings import server_labels from dzgui.util.strings import server_labels
@@ -16,6 +15,7 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.controllers.mc import Emitter
class ServerNotebook(Gtk.ScrolledWindow): class ServerNotebook(Gtk.ScrolledWindow):