chore: ServerModelManager WIP

This commit is contained in:
aclist committed 2026-02-22 01:48:23 +09:00
1 parent 473dcafb26
commit 99ede36a32
3 files changed
+240 -253

No files matched your search

+223 -247
View File
@@ -304,6 +304,7 @@ class Controller(GObject.GObject):
self.mediator.modtreeview.set_model(model) self.mediator.modtreeview.set_model(model)
self.emitter.emit("mod_page_loaded") self.emitter.emit("mod_page_loaded")
# TODO: delegate to threadmanager
@call_on_thread(strings.dialog.modlist) @call_on_thread(strings.dialog.modlist)
def load_mods(self) -> None: def load_mods(self) -> None:
model = ModelFactory().make_mod_store() #self.model_man.new_mod_store() model = ModelFactory().make_mod_store() #self.model_man.new_mod_store()
@@ -375,165 +376,165 @@ class Controller(GObject.GObject):
self.open_page(button.opens) self.open_page(button.opens)
def dump_favorites(self) -> None: # def dump_favorites(self) -> None:
# TODO: if list is empty, return failure # # TODO: if list is empty, return failure
ips = self.query_config(Preferences.IP_LIST) # ips = self.query_config(Preferences.IP_LIST)
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
# TODO: add custom statusbar parameter # # TODO: add custom statusbar parameter
self.cleanup_func = StoredFunc(self.cleanup_on_failure, False) # self.cleanup_func = StoredFunc(self.cleanup_on_failure, False)
return # return
self.dump_ips(ips) # self.dump_ips(ips)
def dump_history(self) -> None: # def dump_history(self) -> None:
history = self.prefs.paths.history # history = self.prefs.paths.history
# TODO: customize statusbar to mention how records are added # # TODO: customize statusbar to mention how records are added
try: # try:
with open(history, "r") as f: # with open(history, "r") as f:
rows = [row.rstrip("\n") for row in f] # rows = [row.rstrip("\n") for row in f]
except OSError: # except OSError:
self.cleanup_func = StoredFunc(self.cleanup_on_failure, False) # self.cleanup_func = StoredFunc(self.cleanup_on_failure, False)
return # return
if len(rows) == 0: # if len(rows) == 0:
self.cleanup_func = StoredFunc(self.cleanup_on_failure, False) # self.cleanup_func = StoredFunc(self.cleanup_on_failure, False)
return # return
self.dump_ips(rows) # self.dump_ips(rows)
def dump_ips(self, ips: list) -> None: # def dump_ips(self, ips: list) -> None:
# NOTE: block malformed records (TODO: add github issue no.) # # NOTE: block malformed records (TODO: add github issue no.)
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] != ""]
with ThreadPoolExecutor() as executor: # with ThreadPoolExecutor() as executor:
futures = [ # futures = [
executor.submit( # executor.submit(
Servers.query_direct, # Servers.query_direct,
ip.split(":")[0], # ip.split(":")[0],
int(ip.split(":")[2]), # int(ip.split(":")[2]),
) # )
for ip in ips # for ip in ips
] # ]
# TODO: update dialog in main loop # # TODO: update dialog in main loop
# wait(futures) # # wait(futures)
serv = [] # serv = []
for future in as_completed(futures): # for future in as_completed(futures):
res = future.result() # res = future.result()
# NOTE: discard failing entries # # NOTE: discard failing entries
if res is None: # if res is None:
continue # continue
serv.append(res) # serv.append(res)
if len(serv) == 0: # if len(serv) == 0:
self.cleanup_func = StoredFunc(self.cleanup_on_failure) # self.cleanup_func = StoredFunc(self.cleanup_on_failure)
return # return
parsed = Servers.parse_json(serv) # parsed = Servers.parse_json(serv)
self.push_data_success(parsed, FilterMode.INITIAL) # self.push_data_success(parsed, FilterMode.INITIAL)
# TODO: strings # TODO: strings
@call_on_thread("scanning LAN ports") # @call_on_thread("scanning LAN ports")
def dump_lan(self, port: int, early_abort: bool) -> None: # def dump_lan(self, port: int, early_abort: bool) -> None:
serv = [] # serv = []
event = threading.Event() # event = threading.Event()
with ThreadPoolExecutor() as executor: # with ThreadPoolExecutor() as executor:
futures = [ # futures = [
executor.submit(Servers.test_ip, i, port, event) for i in range(1, 256) # executor.submit(Servers.test_ip, i, port, event) for i in range(1, 256)
] # ]
for future in as_completed(futures): # for future in as_completed(futures):
try: # try:
res = future.result(timeout=0.5) # res = future.result(timeout=0.5)
if res is not None and early_abort is True: # if res is not None and early_abort is True:
event.set() # event.set()
self.cleanup_func = StoredFunc(self.cleanup_on_failure) # self.cleanup_func = StoredFunc(self.cleanup_on_failure)
return # return
if res is None: # if res is None:
continue # continue
serv.append(res) # serv.append(res)
except Exception as e: # except Exception as e:
logger.critical(e) # logger.critical(e)
self.cleanup_func = StoredFunc(self.cleanup_on_failure) # self.cleanup_func = StoredFunc(self.cleanup_on_failure)
if len(serv) == 0: # if len(serv) == 0:
self.cleanup_func = StoredFunc(self.cleanup_on_failure) # self.cleanup_func = StoredFunc(self.cleanup_on_failure)
return # return
parsed = Servers.parse_json(serv) # parsed = Servers.parse_json(serv)
self.push_data_success(parsed, FilterMode.INITIAL) # self.push_data_success(parsed, FilterMode.INITIAL)
def query_ip_id(self, addr: str) -> None: # def query_ip_id(self, addr: str) -> None:
# NOTE: Battlemetrics # # NOTE: Battlemetrics
# TODO: stores full ip, gameport, qport in file as canonical record # # TODO: stores full ip, gameport, qport in file as canonical record
if addr.isdigit(): # if addr.isdigit():
config = self.prefs.paths.config # config = self.prefs.paths.config
resolved = map_id_to_record(config, addr) # resolved = map_id_to_record(config, addr)
res = Servers.query_direct(resolved.ip, resolved.qport) # res = Servers.query_direct(resolved.ip, resolved.qport)
else: # else:
record = addr.split(":") # record = addr.split(":")
ip, qport = record[0], record[1] # ip, qport = record[0], record[1]
res = Servers.query_direct(ip, int(qport)) # res = Servers.query_direct(ip, int(qport))
return res # return res
# TODO: strings # # TODO: strings
@call_on_thread("querying address") # @call_on_thread("querying address")
def connect_by_id_or_ip(self, addr: str) -> None: # def connect_by_id_or_ip(self, addr: str) -> None:
res = self.query_ip_id(addr) # res = self.query_ip_id(addr)
def cleanup_on_insert(self) -> None: # def cleanup_on_insert(self) -> None:
# FIXME: grab filterman outside of thread # # FIXME: grab filterman outside of thread
treeview = self.mediator.servers.saved # treeview = self.mediator.servers.saved
filter_man = treeview.get_filter_man() # filter_man = treeview.get_filter_man()
model = filter_man.get_control() # model = filter_man.get_control()
print(len(model)) # print(len(model))
# FIXME: refiltration should occur in thread # # FIXME: refiltration should occur in thread
model.append(self.insert_record[0]) # model.append(self.insert_record[0])
filtered = filter_man.filter(FilterMode.INITIAL) # filtered = filter_man.filter(FilterMode.INITIAL)
treeview.set_model(filter_man.get_proxy_model()) # treeview.set_model(filter_man.get_proxy_model())
# TODO: update statusbar # # TODO: update statusbar
context = self.get_active_context() # context = self.get_active_context()
# 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 # # TODO: if all filters are already applied, strange behavior may occur
# need to insert and update all according logic per current filters # # need to insert and update all according logic per current filters
self.emitter.emit("servers_loaded", context) # self.emitter.emit("servers_loaded", context)
# model = treeview.get_model().append(self.insert_record[0]) # # model = treeview.get_model().append(self.insert_record[0])
@call_on_thread("querying address") # @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 = self.query_ip_id(addr)
self.insert_record = Servers.parse_json([res]) # self.insert_record = Servers.parse_json([res])
# if res is None: # # if res is None:
# # TODO: set cleanupfunc with dialog # # # TODO: set cleanupfunc with dialog
# return # # return
# TODO: add into saved servers file # # TODO: add into saved servers file
# TODO: update saved servers model # # TODO: update saved servers model
# if current focus is not save servers, update label # # if current focus is not save servers, update label
# TODO: saved servers might not be loaded yet, in which case # # TODO: saved servers might not be loaded yet, in which case
# should just add to server file # # should just add to server file
self.set_cleanup_func(StoredFunc(self.cleanup_on_insert)) # self.set_cleanup_func(StoredFunc(self.cleanup_on_insert))
def dump_api(self) -> None: # def dump_api(self) -> None:
key = self.query_config(Preferences.STEAM) # key = self.query_config(Preferences.STEAM)
job = Servers.query_api # job = Servers.query_api
params = Servers.params # params = Servers.params
serv = [] # serv = []
with ThreadPoolExecutor() as executor: # with ThreadPoolExecutor() as executor:
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params] # futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
for future in as_completed(futures): # for future in as_completed(futures):
try: # try:
GLib.idle_add(lambda: self.wait_dialog.increment()) # GLib.idle_add(lambda: self.wait_dialog.increment())
res = future.result(timeout=3) # res = future.result(timeout=3)
if res.status != 200 or not res.parsed: # if res.status != 200 or not res.parsed:
self.cleanup_func = StoredFunc(self.cleanup_on_failure) # self.cleanup_func = StoredFunc(self.cleanup_on_failure)
return # return
j = res.json # j = res.json
serv += j["response"]["servers"] # serv += j["response"]["servers"]
except Exception as e: # except Exception as e:
logger.critical(e) # logger.critical(e)
self.cleanup_func = StoredFunc(self.cleanup_on_failure) # self.cleanup_func = StoredFunc(self.cleanup_on_failure)
return # return
# NOTE: This step is allowed to fail, since this metadata is incidental # # NOTE: This step is allowed to fail, since this metadata is incidental
res = Servers.query_api(key, APPID_DAYZ_EXP, "") # res = Servers.query_api(key, APPID_DAYZ_EXP, "")
if res.status == 200 and res.parsed is True: # if res.status == 200 and res.parsed is True:
j = res.json # j = res.json
serv += j["response"]["servers"] # serv += j["response"]["servers"]
GLib.idle_add(lambda: self.wait_dialog.increment("Unpacking servers")) # GLib.idle_add(lambda: self.wait_dialog.increment("Unpacking servers"))
parsed = Servers.parse_json(serv) # parsed = Servers.parse_json(serv)
self.push_data_success(parsed, FilterMode.INITIAL) # self.push_data_success(parsed, FilterMode.INITIAL)
def get_help_row(self) -> str: def get_help_row(self) -> str:
return self.mediator.menu.get_row_enum() return self.mediator.menu.get_row_enum()
@@ -548,6 +549,7 @@ class Controller(GObject.GObject):
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version) remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
# TODO: strings # TODO: strings
# TODO: delegate to LocalModManager
@call_on_thread("deleting mod") @call_on_thread("deleting mod")
def delete_single_mod(self, tree_path: Gtk.TreePath) -> None: def delete_single_mod(self, tree_path: Gtk.TreePath) -> None:
config = self.prefs.paths.config config = self.prefs.paths.config
@@ -671,84 +673,84 @@ class Controller(GObject.GObject):
def get_map_man(self) -> "MapManager": def get_map_man(self) -> "MapManager":
return self.get_active_treeview().get_map_man() return self.get_active_treeview().get_map_man()
def cleanup_on_success(self) -> None: # def cleanup_on_success(self) -> None:
self.pending_jobs = 1 # self.pending_jobs = 1
treeview = self.get_active_treeview() # treeview = self.get_active_treeview()
treeview.set_loaded(True) # treeview.set_loaded(True)
treeview.set_model(self.to_insert) # treeview.set_model(self.to_insert)
# TODO: this will allow history and saved tab to emit signals to statusbar # # TODO: this will allow history and saved tab to emit signals to statusbar
# CHORE: test if treeview's sort method inserts row at the correct index # # CHORE: test if treeview's sort method inserts row at the correct index
# inserting a row serializes file on disk, updates control model for that tab, and # # inserting a row serializes file on disk, updates control model for that tab, and
# reapplies filters to ephemeral model; since filters are applied, in-situ insertion might not be necessary # # reapplies filters to ephemeral model; since filters are applied, in-situ insertion might not be necessary
# TODO: will be inserted out of order # # TODO: will be inserted out of order
# self.to_insert.connect("row-inserted", lambda *args: print("row inserted into model")) # # self.to_insert.connect("row-inserted", lambda *args: print("row inserted into model"))
# TODO: signals or other approach to deferring map # # TODO: signals or other approach to deferring map
# model insertion after thread closes # # model insertion after thread closes
# cf. servers_loaded signal # # cf. servers_loaded signal
# TODO: servers_loaded vs servers_reloaded # # TODO: servers_loaded vs servers_reloaded
context = self.get_active_context() # context = self.get_active_context()
self.emitter.emit("servers_loaded", context) # self.emitter.emit("servers_loaded", context)
# CHORE: this is placeholder logic # # CHORE: this is placeholder logic
if self.first_iteration: # if self.first_iteration:
map_man = treeview.get_map_man() # map_man = treeview.get_map_man()
map_man.set_unique_maps(self.new_maps) # map_man.set_unique_maps(self.new_maps)
self.emitter.emit("servers_loaded_init") # self.emitter.emit("servers_loaded_init")
self.first_iteration = False # self.first_iteration = False
self.new_maps = None # self.new_maps = None
treeview.grab_focus() # treeview.grab_focus()
def cleanup_on_failure(self, show_dialog=True) -> None: # def cleanup_on_failure(self, show_dialog=True) -> None:
treeview = self.get_active_treeview() # treeview = self.get_active_treeview()
treeview.set_loaded(True) # treeview.set_loaded(True)
map_man = treeview.get_map_man() # map_man = treeview.get_map_man()
# TODO: disable map, keyword, and filter widgets if model is None # # TODO: disable map, keyword, and filter widgets if model is None
# -> signal driven (servers_empty) # # -> signal driven (servers_empty)
# TODO: what if refresh action occurred and failed, and the old model is still valid? # # TODO: what if refresh action occurred and failed, and the old model is still valid?
# skip the step below if refresh action failed # # skip the step below if refresh action failed
# do not wipe control model in this case # # do not wipe control model in this case
# e.g. if treeview.is_refresh(): # # e.g. if treeview.is_refresh():
# revert old model # # revert old model
# wipe refresh state to False # # wipe refresh state to False
treeview.set_model(None) # treeview.set_model(None)
treeview.grab_focus() # treeview.grab_focus()
map_man.set_unique_maps(None) # map_man.set_unique_maps(None)
context = self.get_active_context() # context = self.get_active_context()
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" # # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
# customize statusbar accordingly # # customize statusbar accordingly
self.emitter.emit("servers_loaded", context) # self.emitter.emit("servers_loaded", context)
if show_dialog: # if show_dialog:
dialog = ExceptionDialog(self, strings.api_warn_msg) # dialog = ExceptionDialog(self, strings.api_warn_msg)
dialog.run() # dialog.run()
# TODO: break into initial dump and refilter modes, can drop filtermode kwarg # # TODO: break into initial dump and refilter modes, can drop filtermode kwarg
# and stop pushing empty data # # and stop pushing empty data
def push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None: # def push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None:
# FIXME: calls treeview read methods in thread # # FIXME: calls treeview read methods in thread
# treeview = self.get_active_treeview() # # treeview = self.get_active_treeview()
# manager = treeview.get_filter_man() # # manager = treeview.get_filter_man()
manager = self.get_filter_man() # manager = self.get_filter_man()
if data is None: # if data is None:
self.to_insert = None # self.to_insert = None
else: # else:
if mode == FilterMode.INITIAL: # if mode == FilterMode.INITIAL:
manager.set_control(data) # manager.set_control(data)
manager.filter(mode) # manager.filter(mode)
self.to_insert = manager.get_model() # self.to_insert = manager.get_model()
# TODO: pre parse maps # # TODO: pre parse maps
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.new_maps = sorted(u_maps)
self.cleanup_func = StoredFunc(self.cleanup_on_success) # self.cleanup_func = StoredFunc(self.cleanup_on_success)
def highlight_stale_cleanup(self, stale_mods: list) -> None: def highlight_stale_cleanup(self, stale_mods: list) -> None:
"""Manipulates attached ListStore in the main event loop""" """Manipulates attached ListStore in the main event loop"""
@@ -848,18 +850,14 @@ class Controller(GObject.GObject):
def get_statusbar(self) -> None: def get_statusbar(self) -> None:
return self.mediator.statusbar return self.mediator.statusbar
@call_on_thread(strings.dialog.fetching) # @call_on_thread(strings.dialog.filtering)
def run_query_func(self, func: StoredFunc) -> None: # def filter_threaded(
func.call() # self, filter_man: "FilteredModelManager", mode: FilterMode, label: str
# ) -> None:
@call_on_thread(strings.dialog.filtering) # filter_man.filter(mode, label)
def filter_threaded( # self.to_insert = filter_man.get_proxy_model()
self, filter_man: "FilteredModelManager", mode: FilterMode, label: str # print("filtering threaded")
) -> None: # self.cleanup_func = StoredFunc(self.cleanup_on_success)
filter_man.filter(mode, label)
self.to_insert = filter_man.get_proxy_model()
print("filtering threaded")
self.cleanup_func = StoredFunc(self.cleanup_on_success)
# TODO: call filter_man methods directly # TODO: call filter_man methods directly
def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None: def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None:
@@ -877,30 +875,8 @@ 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:
# NOTE: prepare GTK objects outside of thread
#if tv.is_loaded() is True:
# self.emitter.emit("servers_loaded", tv.get_enum())
# return
#func, jobs = tv.get_query_func()
ServerModelManager(self, tv, first_iteration=True) ServerModelManager(self, tv, first_iteration=True)
#if func is None:
# self.emitter.emit("servers_loaded", tv.get_enum())
# tv.set_model(None)
# return
# TODO: clear ephemeral model if necessary
# manager = treeview.get_filter_man()
# manager.clear_model()
# self.first_iteration = True
# self.pending_jobs = jobs
# # TODO: get filter manager a priori and set it so that it can be accessed out of thread
# filter_man = treeview.get_filter_man()
# self.set_filter_man(filter_man)
# self.run_query_func(func)
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))
if len(fav) < 1: if len(fav) < 1:
+17
View File
@@ -322,3 +322,20 @@ class ServerModelManager:
def _get_filter_man(self) -> "FilteredModelManager": def _get_filter_man(self) -> "FilteredModelManager":
return self.filter_man return self.filter_man
# TODO: unimplemented
# @call_on_thread(strings.dialog.filtering)
# def filter_threaded(
# self, filter_man: "FilteredModelManager", mode: FilterMode, label: str
# ) -> None:
# filter_man.filter(mode, label)
# self.to_insert = filter_man.get_proxy_model()
# print("filtering threaded")
# self.cleanup_func = StoredFunc(self.cleanup_on_success)
# TODO: call filter_man methods directly
#def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None:
# tv = self.get_active_treeview()
# filter_man = tv.get_filter_man()
# if filter_man.get_control() is None:
# return
# self.filter_threaded(filter_man, mode, label)
-6
View File
@@ -37,12 +37,6 @@ class ServerNotebook(Gtk.ScrolledWindow):
) )
self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN) self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN)
# TODO: number of jobs will not be known a priori unless files are read
self.recent.set_query_func(StoredFunc(self.controller.dump_history))
self.saved.set_query_func(StoredFunc(self.controller.dump_favorites))
self.lan.set_query_func(StoredFunc(self.controller.dump_test_2))
self.browser.set_query_func(StoredFunc(self.controller.dump_api), jobs=11)
tabs = [ tabs = [
(self.browser, server_labels.browser), (self.browser, server_labels.browser),
(self.saved, server_labels.saved), (self.saved, server_labels.saved),