diff --git a/lib/client/__init__.py b/lib/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/client/client.py b/lib/client/client.py index 21031b9..227f04a 100644 --- a/lib/client/client.py +++ b/lib/client/client.py @@ -5,11 +5,13 @@ import tkinter as tk import threading import time import socket -import os +import os, sys from tkinter import ttk, messagebox from PIL import Image, ImageTk from new_job_window import NewJobWindow from server_proxy import RenderServerProxy +sys.path.append("../") +from lib.server.zeroconf_server import ZeroconfServer def request_data(server_ip, payload, server_port=8080, timeout=2): @@ -44,15 +46,17 @@ class ZordonClient: def __init__(self): - servers = available_servers() - # Create a Treeview widget self.root = tk.Tk() self.root.title("Zordon Render Client") self.local_host = socket.gethostname() - self.server_proxy = RenderServerProxy(hostname=servers[0]) + self.server_proxy = RenderServerProxy(hostname=self.local_host) self.job_cache = [] + # Setup zeroconf + self.zeroconf = ZeroconfServer("_zordon._tcp.local.", socket.gethostname(), 8080) + self.zeroconf.start(listen_only=True) + # Setup photo preview photo_pad = tk.Frame(self.root, background="gray") photo_pad.pack(fill=tk.BOTH, pady=5, padx=5) @@ -129,7 +133,7 @@ class ZordonClient: pass # update servers - self.populate_server_tree() + self.update_servers() try: selected_server = self.server_tree.get_children()[0] self.server_tree.selection_set(selected_server) @@ -141,15 +145,12 @@ class ZordonClient: x.daemon = True x.start() - def populate_server_tree(self): - servers = available_servers() - self.server_tree.delete(*self.server_tree.get_children()) - for hostname in servers: - self.server_tree.insert("", tk.END, iid=hostname, values=(hostname,)) - def server_picked(self, event): - new_hostname = self.server_tree.selection()[0] - self.server_proxy.hostname = new_hostname + try: + new_hostname = self.server_tree.selection()[0] + self.server_proxy.hostname = new_hostname + except IndexError: + pass self.job_cache.clear() self.update_jobs(clear_table=True) @@ -218,38 +219,56 @@ class ZordonClient: def __background_update(self): while True: self.update_jobs() - time.sleep(1) + self.update_servers() + time.sleep(3) + + def update_servers(self): + servers = self.zeroconf.found_clients() + if len(servers) < len(self.server_tree.get_children()): + self.server_tree.delete(*self.server_tree.get_children()) + for hostname in servers: + if hostname not in self.server_tree.get_children(): + self.server_tree.insert("", tk.END, iid=hostname, values=(hostname,)) def update_jobs(self, clear_table=False): - def update_row(tree, id, new_values, tags=None): - for item in tree.get_children(): - values = tree.item(item, "values") - if values[0] == id: - tree.item(item, values=new_values, tags=tags) - break - job_fetch = self.server_proxy.get_jobs() - if job_fetch is not None: - if len(job_fetch) < len(self.job_cache) or len(job_fetch) == 0: - clear_table = True - self.job_cache = job_fetch # update the cache only if its good data + def update_jobs_inner(): + def update_row(tree, id, new_values, tags=None): + for item in tree.get_children(): + values = tree.item(item, "values") + if values[0] == id: + tree.item(item, values=new_values, tags=tags) + break + job_fetch = self.server_proxy.get_jobs() + if job_fetch is not None: + if len(job_fetch) < len(self.job_cache) or len(job_fetch) == 0: + self.job_tree.delete(*self.job_tree.get_children()) + self.job_cache = job_fetch # update the cache only if its good data + for job in self.job_cache: + display_status = job['status'] if job['status'] != 'running' else \ + ('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percentage, otherwise just show status + tags = (job['status'],) + values = (job['id'], + job['name'] or os.path.basename(job['input_path']), + job['renderer'] + "-" + job['renderer_version'], + job['priority'], + display_status, + job['time_elapsed'], + job['total_frames']) + try: + if self.job_tree.exists(job['id']): + update_row(self.job_tree, job['id'], new_values=values, tags=tags) + else: + self.job_tree.insert("", tk.END, iid=job['id'], values=values, tags=tags) + except tk.TclError: + pass + if clear_table: self.job_tree.delete(*self.job_tree.get_children()) - for job in self.job_cache: - display_status = job['status'] if job['status'] != 'running' else \ - ('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percentage, otherwise just show status - tags = (job['status'],) - values = (job['id'], - job['name'] or os.path.basename(job['input_path']), - job['renderer'] + "-" + job['renderer_version'], - job['priority'], - display_status, - job['time_elapsed'], - job['total_frames']) - if self.job_tree.exists(job['id']): - update_row(self.job_tree, job['id'], new_values=values, tags=tags) - else: - self.job_tree.insert("", tk.END, iid=job['id'], values=values, tags=tags) + + x = threading.Thread(target=update_jobs_inner) + x.daemon = True + x.start() def show_new_job_window(self): new_window = tk.Toplevel(self.root) diff --git a/lib/client/new_job_window.py b/lib/client/new_job_window.py index b2b3d77..1fa4f99 100755 --- a/lib/client/new_job_window.py +++ b/lib/client/new_job_window.py @@ -11,7 +11,8 @@ from tkinter.ttk import Frame, Label, Entry, Combobox import psutil import requests - +import sys +sys.path.append('../../') from lib.render_workers.blender_worker import Blender from server_proxy import RenderServerProxy diff --git a/lib/client/server_proxy.py b/lib/client/server_proxy.py index 92a226a..667176a 100644 --- a/lib/client/server_proxy.py +++ b/lib/client/server_proxy.py @@ -34,15 +34,16 @@ class RenderServerProxy: def request(self, payload, timeout=5): return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout) - def get_jobs(self): - all_jobs = self.request_data('jobs') - sorted_jobs = [] - if all_jobs: + def get_jobs(self, timeout=5): + all_jobs = self.request_data('jobs', timeout=timeout) + if all_jobs is not None: + sorted_jobs = [] for status_category in categories: found_jobs = [x for x in all_jobs if x['status'] == status_category.value] if found_jobs: sorted_jobs.extend(found_jobs) - return sorted_jobs + all_jobs = sorted_jobs + return all_jobs def get_data(self, timeout=5): all_data = self.request_data('full_status', timeout=timeout) diff --git a/lib/server/job_server.py b/lib/server/job_server.py index 178f6f4..6c8a720 100755 --- a/lib/server/job_server.py +++ b/lib/server/job_server.py @@ -16,6 +16,7 @@ import yaml from flask import Flask, request, render_template, send_file, after_this_request, Response, redirect, url_for, abort from werkzeug.utils import secure_filename +from lib.server.zeroconf_server import ZeroconfServer from lib.render_queue import RenderQueue, JobNotFoundError from lib.render_workers.worker_factory import RenderWorkerFactory from lib.render_workers.base_worker import string_to_status, RenderStatus @@ -504,6 +505,9 @@ def start_server(background_thread=False): logging.info(f"Starting Zordon Render Server - Hostname: '{RenderQueue.hostname}'") + zeroconf_server = ZeroconfServer("_zordon._tcp.local.", RenderQueue.hostname, RenderQueue.port) + zeroconf_server.start() + if background_thread: server_thread = threading.Thread( target=lambda: server.run(host='0.0.0.0', port=RenderQueue.port, debug=False, use_reloader=False)) diff --git a/lib/server/zeroconf_server.py b/lib/server/zeroconf_server.py new file mode 100644 index 0000000..80334bf --- /dev/null +++ b/lib/server/zeroconf_server.py @@ -0,0 +1,73 @@ +import logging +import socket + +from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser, ServiceStateChange + +logger = logging.getLogger() + + +class ZeroconfServer(): + def __init__(self, service_type, server_name, server_port): + self.service_type = service_type + self.server_name = server_name + self.server_port = server_port + self.server_ip = None + self.zeroconf = Zeroconf() + self.service_info = None + self.client_cache = {} + self.properties = {} + + def start(self, listen_only=False): + if not listen_only: + self._register_service() + self._browse_services() + + def stop(self): + self._unregister_service() + self.zeroconf.close() + + def _register_service(self): + self.server_ip = socket.gethostbyname(socket.gethostname()) + + info = ServiceInfo( + self.service_type, + f"{self.server_name}.{self.service_type}", + addresses=[socket.inet_aton(self.server_ip)], + port=self.server_port, + properties=self.properties, + ) + + self.service_info = info + self.zeroconf.register_service(info) + logger.info(f"Registered zeroconf service: {self.service_info.name}") + + def _unregister_service(self): + if self.service_info: + self.zeroconf.unregister_service(self.service_info) + logger.info(f"Unregistered zeroconf service: {self.service_info.name}") + self.service_info = None + + def _browse_services(self): + browser = ServiceBrowser(self.zeroconf, self.service_type, [self._on_service_discovered]) + + def _on_service_discovered(self, zeroconf, service_type, name, state_change): + info = zeroconf.get_service_info(service_type, name) + logger.debug(f"Zeroconf: {name} {state_change}") + if service_type == self.service_type: + if state_change == ServiceStateChange.Added or state_change == ServiceStateChange.Updated: + self.client_cache[name] = info + else: + self.client_cache.pop(name) + + def found_clients(self): + return [x.split(f'.{self.service_type}')[0] for x in self.client_cache.keys()] + + +# Example usage: +if __name__ == "__main__": + server = ZeroconfServer("_zordon._tcp.local.", "foobar.local", 8080) + try: + server.start() + input("Server running - Press enter to end") + finally: + server.stop() diff --git a/requirements.txt b/requirements.txt index 868b63a..48d1a88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ tkinterdnd2~=0.3.0 future==0.18.3 json2html~=1.3.0 SQLAlchemy~=2.0.15 -Pillow~=9.3.0 \ No newline at end of file +Pillow~=9.3.0 +zeroconf~=0.63.0 \ No newline at end of file