diff --git a/resources/icons/Monitor.png b/resources/icons/Monitor.png index 46bbf88..15449a0 100644 Binary files a/resources/icons/Monitor.png and b/resources/icons/Monitor.png differ diff --git a/resources/icons/linux.png b/resources/icons/linux.png new file mode 100644 index 0000000..070eb3c Binary files /dev/null and b/resources/icons/linux.png differ diff --git a/resources/icons/macos.png b/resources/icons/macos.png new file mode 100644 index 0000000..94973d1 Binary files /dev/null and b/resources/icons/macos.png differ diff --git a/resources/icons/windows.png b/resources/icons/windows.png new file mode 100644 index 0000000..8c49082 Binary files /dev/null and b/resources/icons/windows.png differ 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__":