From 06a613fcc4ecdecb695796a9a5aef46591e96891 Mon Sep 17 00:00:00 2001 From: Brett Date: Sat, 4 Nov 2023 20:46:27 -0500 Subject: [PATCH] Zeroconf reports system properties (#59) * Zeroconf.found_clients() now returns dicts of clients, not just hostnames * Adjustments to distributed_job_manager.py * Undo config change * Report system metrics (cpu, os, etc) via zeroconf_server.py * Zeroconf.found_clients() now returns dicts of clients, not just hostnames * Adjustments to distributed_job_manager.py * Undo config change * Zeroconf.found_clients() now returns dicts of clients, not just hostnames * Adjustments to distributed_job_manager.py * Undo config change * Adjustments to distributed_job_manager.py * Undo config change * Rename ZeroconfServer.found_clients() to found_hostnames() --- resources/icons/Monitor.png | Bin 249 -> 450 bytes resources/icons/linux.png | Bin 0 -> 1751 bytes resources/icons/macos.png | Bin 0 -> 1483 bytes resources/icons/windows.png | Bin 0 -> 806 bytes src/api/api_server.py | 6 +++++- src/distributed_job_manager.py | 17 ++++++++++------- src/ui/add_job.py | 2 +- src/ui/main_window.py | 28 +++++++++------------------- src/utilities/zeroconf_server.py | 18 +++++++++++++----- 9 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 resources/icons/linux.png create mode 100644 resources/icons/macos.png create mode 100644 resources/icons/windows.png diff --git a/resources/icons/Monitor.png b/resources/icons/Monitor.png index 46bbf88ae201ceed306477857b1d5973a27f5285..15449a065b2a4843b57d2f31d2f700fffa532729 100644 GIT binary patch literal 450 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lNlHo%ROBj zLn`LHy|dBlaDYg|!{Wux;qtCBEa|t^3pCbs<0O~q-FiYFN9?z;4?VQur|W&N!>UB_Ra4tu`kRkq@+KE?#L z2Ho<%jeie+-XC%N&aRzSuS@n=Go&%h0Le&FpKknBfeA0rLSK2JhM+iQezm7O&Eu)v>iB;^Z9L*7HIi}1n BwyXdE literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Y)RhkE)4%caKYZ?lYt`ZJY5_^ zD(1YsZ76uyK!nxtBoC94$f0vnPqsEY?(m*Hp&*U>F4KiOdw#Ax=CNth&#L-;R!+A{ z8m=)|Ib<`)E--E2yCCJjp253-c?;VG#wg|&3~L!Fk&fw|l=d#Wzp$PzUNLrBq diff --git a/resources/icons/linux.png b/resources/icons/linux.png new file mode 100644 index 0000000000000000000000000000000000000000..070eb3cee68039792935252c55ed23034301c7cf GIT binary patch literal 1751 zcmV;|1}OQ7P)BWK~#90?b~Tg9OoU!@$WOUyBr3C7Yz6SFIND^2X0bh*^ceR@zJW4 znp08wqU!yws?J5Ji#IK*~hOEuTBz+!MPyUXHb zmp$j{3t}rSX3es&Gvhv=S7>J*=D+h}X8tqJ0E(h0ilQirq9}@@D2k#eilRK~M6Lr~ z{MHZTS`_lAcgOjXG@WSHJjCUgOmfU@{ee z$y5L)QvsMv1z@tgyP!e~-i~pl$B%CyNIGp$=`LaC6Pwue8{*V& zu98k0_dbh8lBALu`mRm!(w95Yb$cDuwyO5*UkC5o`2f1E;c_|f_64}~&X`G+t(EL}+Ur@NeF8{jK)}Ak+t|`j0f48~&C6fuWP4{FLKEBx z#|hq!nOtdZY!iTCNVYX?U{n2io;+9&z=`KNxD!tB)QM*J_z!MqwpKvgS%cTRLY4+-bY|{l5mpF+=64ca{arNR&QmG7Mz8Rb@ zoxoHK&7na8LVP4xMbFvZ{n+JWX}&I==p$eROthVaJ|2x?k%fawkF0JELZoyKJ_&21p5SobKnb z$7^u8bUyd;a?OSskIoY!0I_oQn zY1>r`Kqh1Gmp@)7namWb#1*rA0OKQJ&Ubs!G>Ao$S=ZhD>CN|k4o=57cV=L%%gdXk z1CWNK`*a^jN!!j^bVv4bo7$?V*<8+ssuF~NtCuFYIT|idwJT%k06y%UB$Y_hzO$BR zKmQn|>s(pa7ea9084ux5oJw~IQcB+b`%rH*iG+mHR zOLR@(_suXlHoI13=FPGJcy9zqrwq#07qPLfY}w;fn?GJL`Xd}9=G3pKB_swwlWQ!%|3M>=A#((HG+S1#P-_;YR4H&#Ay`SspO zrY0A^HF1|=NY0)aV0ge<6E>DP1x4qwPo`%w25-GSNMmaiMa2tcr&1ZlePMe4J`u>YCHtlg8DEE9mx1kLT$G`Cj+kW6M6?hi2ZpJ|5s1H@t}_CM`m z_ukEPc0IV88665SJQyS%OHp0Bj?cch-STfpSSEn~<#OqC?5d?>S1nRX(n$knQT7`u zPpg~n{^V&=sf?v>zCOt^T~Ibch==a{PqNF-2b%=&C{qELOa)*v6@ba|?t=3DPx1eE z>Ig8I3czG40F$WzOr`=bnMI!wE_UqIujMqA_12$$Ym4uujsTOX08FL=FqsO#WGVoY z6|_@0{>nGH(2So-cmlB6T;Ys>D;U}j-#qo}_X@Sqf(7vGD_`9sG;tBg{dMQUic3T8 zKYQwT9~5F^1v>=^U3?$w?|gt_H1WMcZLDzD0Lig}Z7WX#ju&cMg}ew`?VO)Y!D>}q tkfJDxq9}@@D2k#eilQirqA1E5`!57X4)Uh503-ka002ovPDHLkV1l9ZXR-hQ literal 0 HcmV?d00001 diff --git a/resources/icons/macos.png b/resources/icons/macos.png new file mode 100644 index 0000000000000000000000000000000000000000..94973d18c5ac4a9b6e804ff08d8b6c12c407d891 GIT binary patch literal 1483 zcmV;+1vL7JP)>;5{n!I*P%f7~z=hA^Fi8D&Z! zND!Zt_+X5YM-2%Nd(a1ckS8O=KZ^v7L5XfWFqTQC0TBfpVZjW+IazTG=+<`FKeYGy zfWa{a*=f(+J>7nvWDnbNemyzgo^x-{y|(}%gb+dqA%qY@2qA>b2P~0BEnByhKv8yS zU@Z)Aitbb@(Sab-(W9!`j;?*54)C_;4xnZ0wh~=Y`v7F~ww>dA7KZvmpMN~e(@5cd z8AaIz8vN;G$8`2SZzJviG_V%lHuK0@-Ub%l22V=!OL$fH@iDrQa*s6e;7i~!Sm2G9mSP|8KvPRpgl1o5ZB-F(4k-(9?PT_V4y|D;ic7o7khoq-+3}2FDTb zhXDYJ!m$2@YB-!}CijY8$}T};*Fyk+g2GI!-Bg7|Wp0yulo+W4h=gPK^@JDa`$yp) z55eKEVNsbIm30NEsLn;{jwj%9r{Ti6Yv}l*A0yrXqLDb4GmoEkI)AvKLr(5A%V8ggCjGNK;9c}$%A^ds4hm%MD!seIi zk(2M3?e5Q*ly8qK-XQkvK6%IBPZbW`#MO}-cMbkzS4SqWZ}%xg!Z9PdDTH(Z#A6!1 z{;co513Ob`b_-gcuR@w#+KDb-0B2AAeft2LtaY0n#zQrELTVXBx&Q|HM}^cWEOw!` zVWE&(Mv^XoE8d`xIu*6~Lh2Yzx&UqjBSPwArpSjf=>lL%Lbod2B^s6Y%x5y`0!Vk* zgwz=x92Zi@Xwn6clb0c+&OrYtu8;mBq?VDS3qaJKzNYEu{PHZK0zOX|NxA?YtjZNu z%RA)9-Vc7kvCn+H@^w1Cz^BtFa|WNS|qgQ$~8-b))Ps( z005w2Wii~j=|XCilxCr_wm?WNBS;y5sw#Nqxyt10SQM<=Tn#4XqlF=)3;+NsAIV4k zvgG!1);wE*{K8B@Ju-&00RTYj=4zCdPyZ+aV}L!wde63xHI-oblVXo0Mv!--9JiK` z7hi3_(e8^leS8?2rekq=HXdy(LTPz6REvUGOv4`o*Kqo{7ncUdknXUeX-yfHHWr!P z{TY!u002-GhGtJWnmy&y{@-FzP*IbQikj5e{hUhbC1^gN08B;!n2Z8283kanq|bux zd`gh;q8VT^3czF(fXTS`3BNsZOgEw~gaD@BjjRdx3E_DXd&$qG8L4w_e#G#ipV(*8?h-Ft}mNA_0^1T1x)7IPn7t7E5`sLwTm*?4R z6Eqo&z;YrCk&m=Axc}T2i(|O*m!tIlA#R;l`;J~O?f$*|)Rld`t_-oqZ`M5>+V_QJNyZ{XI`B6et~(%=Y72A*$?GyV=vnOg0bf3%=3-!gC)J(_!W4k zatPZt>y#hS1w%OKL!qN?IU#f{GIJe74VZVVNPTMF02 z7~OtXbGj*nWy5T*lL@=ne6Dxx4eydZE_Z&~_Pf7dvWoQ{c$dBQ$jE&#)L2 zZ?Gs>lilbhke|8c_s4I?Eaq^`^MCQi{+Lq3ToGP_P}3Q!mVHk1U*Gh}j}aafLg9{{ z|25~8&tinQ85~8^&aqC?JrldbV5ueX)+UkJ;kM zrnj~kQV)a|9Xfkm`?P$l=_{Rvo2}i)eGb+zO!)R+q}6Pb`u%k_|KBV!OrNe0Y~tqDo=E+otvE=YFly@<60Bmy{#_81DGLDqWz^ Swhfr@89ZJ6T-G@yGywoRe`g5* literal 0 HcmV?d00001 diff --git a/src/api/api_server.py b/src/api/api_server.py index 4258c68..29b8f32 100755 --- a/src/api/api_server.py +++ b/src/api/api_server.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import json import logging +import multiprocessing import os import pathlib import shutil @@ -277,7 +278,7 @@ def snapshot(): @server.get('/api/_detected_clients') def detected_clients(): # todo: dev/debug only. Should not ship this - probably. - return ZeroconfServer.found_clients() + return ZeroconfServer.found_hostnames() # New version @@ -548,6 +549,9 @@ def start_server(): logger.info(f"Starting Zordon Render Server - Hostname: '{server.config['HOSTNAME']}:'") ZeroconfServer.configure("_zordon._tcp.local.", server.config['HOSTNAME'], server.config['PORT']) + ZeroconfServer.properties = {'system_cpu': current_system_cpu(), 'system_cpu_cores': multiprocessing.cpu_count(), + 'system_os': current_system_os(), + 'system_os_version': current_system_os_version()} ZeroconfServer.start() try: diff --git a/src/distributed_job_manager.py b/src/distributed_job_manager.py index 6fa1e12..1abf944 100644 --- a/src/distributed_job_manager.py +++ b/src/distributed_job_manager.py @@ -220,10 +220,10 @@ class DistributedJobManager: time.sleep(5) @classmethod - def split_into_subjobs(cls, worker, job_data, project_path): + def split_into_subjobs(cls, worker, job_data, project_path, system_os=None): # Check availability - available_servers = cls.find_available_servers(worker.renderer) + available_servers = cls.find_available_servers(worker.renderer, system_os) logger.debug(f"Splitting into subjobs - Available servers: {available_servers}") subjob_servers = cls.distribute_server_work(worker.start_frame, worker.end_frame, available_servers) local_hostname = socket.gethostname() @@ -353,17 +353,20 @@ class DistributedJobManager: return server_breakdown @staticmethod - def find_available_servers(engine_name): + def find_available_servers(engine_name, system_os=None): """ Scan the Zeroconf network for currently available render servers supporting a specific engine. :param engine_name: str, The engine type to search for + :param system_os: str, Restrict results to servers running a specific OS :return: A list of dictionaries with each dict containing hostname and cpu_count of available servers """ available_servers = [] - for hostname in ZeroconfServer.found_clients(): - response = RenderServerProxy(hostname).is_engine_available(engine_name) - if response and response.get('available', False): - available_servers.append(response) + for hostname in ZeroconfServer.found_hostnames(): + host_properties = ZeroconfServer.get_hostname_properties(hostname) + if not system_os or (system_os and system_os == host_properties.get('system_os')): + response = RenderServerProxy(hostname).is_engine_available(engine_name) + if response and response.get('available', False): + available_servers.append(response) return available_servers diff --git a/src/ui/add_job.py b/src/ui/add_job.py index e2aae29..a734905 100644 --- a/src/ui/add_job.py +++ b/src/ui/add_job.py @@ -179,7 +179,7 @@ class NewRenderJobForm(QWidget): self.renderer_type.addItems(self.renderer_info.keys()) def update_server_list(self): - clients = ZeroconfServer.found_clients() + clients = ZeroconfServer.found_hostnames() self.server_input.clear() self.server_input.addItems(clients) diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 99361e9..70e37a1 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -214,25 +214,14 @@ class MainWindow(QMainWindow): # Update the Server Info box when a server is changed self.server_info_hostname.setText(self.current_hostname or "unknown") - if self.current_server_proxy.system_os: - self.server_info_os.setText(f"OS: {self.current_server_proxy.system_os} " - f"{self.current_server_proxy.system_os_version}") - self.server_info_cpu.setText(f"CPU: {self.current_server_proxy.system_cpu} - " - f"{self.current_server_proxy.system_cpu_count} cores") + server_info = ZeroconfServer.get_hostname_properties(self.current_hostname) + if server_info: + self.server_info_os.setText(f"OS: {server_info['system_os']} {server_info['system_os_version']}") + self.server_info_cpu.setText(f"CPU: {server_info['system_cpu']} - {server_info.get('system_cpu_cores')} cores") else: - self.server_info_os.setText(f"OS: Loading...") - self.server_info_cpu.setText(f"CPU: Loading...") + self.server_info_os.setText(f"OS: Unknown") + self.server_info_cpu.setText(f"CPU: Unknown") - def update_server_info_worker(): - server_details = self.current_server_proxy.get_status() - if server_details['hostname'] == self.current_hostname: - self.server_info_os.setText(f"OS: {server_details.get('system_os')} " - f"{server_details.get('system_os_version')}") - self.server_info_cpu.setText(f"CPU: {server_details.get('system_cpu')} - " - f"{server_details.get('cpu_count')} cores") - - update_thread = threading.Thread(target=update_server_info_worker) - update_thread.start() except AttributeError: pass @@ -379,7 +368,7 @@ class MainWindow(QMainWindow): self.image_label.setPixmap(pixmap) def update_servers(self): - found_servers = list(set(ZeroconfServer.found_clients() + self.added_hostnames)) + found_servers = list(set(ZeroconfServer.found_hostnames() + self.added_hostnames)) # Always make sure local hostname is first current_hostname = socket.gethostname() if found_servers and found_servers[0] != current_hostname: @@ -402,7 +391,8 @@ class MainWindow(QMainWindow): current_server_list.append(self.server_list_view.item(i).text()) for hostname in found_servers: if hostname not in current_server_list: - image_path = os.path.join(resources_dir(), 'icons', 'Monitor.png') + properties = ZeroconfServer.get_hostname_properties(hostname) + image_path = os.path.join(resources_dir(), 'icons', f"{properties.get('system_os', 'Monitor')}.png") list_widget = QListWidgetItem(QIcon(image_path), hostname) self.server_list_view.addItem(list_widget) diff --git a/src/utilities/zeroconf_server.py b/src/utilities/zeroconf_server.py index 3686ce0..acd8c91 100644 --- a/src/utilities/zeroconf_server.py +++ b/src/utilities/zeroconf_server.py @@ -1,6 +1,7 @@ import logging import socket +import zeroconf from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser, ServiceStateChange logger = logging.getLogger() @@ -24,6 +25,8 @@ class ZeroconfServer: @classmethod def start(cls, listen_only=False): + if not cls.service_type: + raise RuntimeError("The 'configure' method must be run before starting the zeroconf server") if not listen_only: cls._register_service() cls._browse_services() @@ -49,8 +52,8 @@ class ZeroconfServer: cls.service_info = info cls.zeroconf.register_service(info) logger.info(f"Registered zeroconf service: {cls.service_info.name}") - except socket.gaierror as e: - logger.error(f"Error starting zeroconf service: {e}") + except zeroconf.NonUniqueNameException as e: + logger.error(f"Error establishing zeroconf: {e}") @classmethod def _unregister_service(cls): @@ -75,19 +78,24 @@ class ZeroconfServer: cls.client_cache.pop(name) @classmethod - def found_clients(cls): - + def found_hostnames(cls): fetched_hostnames = [x.split(f'.{cls.service_type}')[0] for x in cls.client_cache.keys()] local_hostname = socket.gethostname() # Define a sort key function def sort_key(hostname): # Return 0 if it's the local hostname so it comes first, else return 1 - return 0 if hostname == local_hostname else 1 + return False if hostname == local_hostname else True # Sort the list with the local hostname first sorted_hostnames = sorted(fetched_hostnames, key=sort_key) return sorted_hostnames + @classmethod + def get_hostname_properties(cls, hostname): + new_key = hostname + '.' + cls.service_type + server_info = cls.client_cache.get(new_key).properties + decoded_server_info = {key.decode('utf-8'): value.decode('utf-8') for key, value in server_info.items()} + return decoded_server_info # Example usage: if __name__ == "__main__":