Merge pull request #7 from blw1138/zeroconf

Zeroconf
This commit is contained in:
2023-06-02 16:11:19 -05:00
committed by GitHub
7 changed files with 146 additions and 47 deletions

0
lib/client/__init__.py Normal file
View File

View File

@@ -5,11 +5,13 @@ import tkinter as tk
import threading import threading
import time import time
import socket import socket
import os import os, sys
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
from PIL import Image, ImageTk from PIL import Image, ImageTk
from new_job_window import NewJobWindow from new_job_window import NewJobWindow
from server_proxy import RenderServerProxy 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): def request_data(server_ip, payload, server_port=8080, timeout=2):
@@ -44,15 +46,17 @@ class ZordonClient:
def __init__(self): def __init__(self):
servers = available_servers()
# Create a Treeview widget # Create a Treeview widget
self.root = tk.Tk() self.root = tk.Tk()
self.root.title("Zordon Render Client") self.root.title("Zordon Render Client")
self.local_host = socket.gethostname() self.local_host = socket.gethostname()
self.server_proxy = RenderServerProxy(hostname=servers[0]) self.server_proxy = RenderServerProxy(hostname=self.local_host)
self.job_cache = [] self.job_cache = []
# Setup zeroconf
self.zeroconf = ZeroconfServer("_zordon._tcp.local.", socket.gethostname(), 8080)
self.zeroconf.start(listen_only=True)
# Setup photo preview # Setup photo preview
photo_pad = tk.Frame(self.root, background="gray") photo_pad = tk.Frame(self.root, background="gray")
photo_pad.pack(fill=tk.BOTH, pady=5, padx=5) photo_pad.pack(fill=tk.BOTH, pady=5, padx=5)
@@ -129,7 +133,7 @@ class ZordonClient:
pass pass
# update servers # update servers
self.populate_server_tree() self.update_servers()
try: try:
selected_server = self.server_tree.get_children()[0] selected_server = self.server_tree.get_children()[0]
self.server_tree.selection_set(selected_server) self.server_tree.selection_set(selected_server)
@@ -141,15 +145,12 @@ class ZordonClient:
x.daemon = True x.daemon = True
x.start() 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): def server_picked(self, event):
new_hostname = self.server_tree.selection()[0] try:
self.server_proxy.hostname = new_hostname new_hostname = self.server_tree.selection()[0]
self.server_proxy.hostname = new_hostname
except IndexError:
pass
self.job_cache.clear() self.job_cache.clear()
self.update_jobs(clear_table=True) self.update_jobs(clear_table=True)
@@ -218,38 +219,56 @@ class ZordonClient:
def __background_update(self): def __background_update(self):
while True: while True:
self.update_jobs() 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_jobs(self, clear_table=False):
def update_row(tree, id, new_values, tags=None): def update_jobs_inner():
for item in tree.get_children(): def update_row(tree, id, new_values, tags=None):
values = tree.item(item, "values") for item in tree.get_children():
if values[0] == id: values = tree.item(item, "values")
tree.item(item, values=new_values, tags=tags) if values[0] == id:
break tree.item(item, values=new_values, tags=tags)
job_fetch = self.server_proxy.get_jobs() break
if job_fetch is not None: job_fetch = self.server_proxy.get_jobs()
if len(job_fetch) < len(self.job_cache) or len(job_fetch) == 0: if job_fetch is not None:
clear_table = True if len(job_fetch) < len(self.job_cache) or len(job_fetch) == 0:
self.job_cache = job_fetch # update the cache only if its good data 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: if clear_table:
self.job_tree.delete(*self.job_tree.get_children()) self.job_tree.delete(*self.job_tree.get_children())
for job in self.job_cache:
display_status = job['status'] if job['status'] != 'running' else \ x = threading.Thread(target=update_jobs_inner)
('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percentage, otherwise just show status x.daemon = True
tags = (job['status'],) x.start()
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)
def show_new_job_window(self): def show_new_job_window(self):
new_window = tk.Toplevel(self.root) new_window = tk.Toplevel(self.root)

View File

@@ -11,7 +11,8 @@ from tkinter.ttk import Frame, Label, Entry, Combobox
import psutil import psutil
import requests import requests
import sys
sys.path.append('../../')
from lib.render_workers.blender_worker import Blender from lib.render_workers.blender_worker import Blender
from server_proxy import RenderServerProxy from server_proxy import RenderServerProxy

View File

@@ -34,15 +34,16 @@ class RenderServerProxy:
def request(self, payload, timeout=5): def request(self, payload, timeout=5):
return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout) return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout)
def get_jobs(self): def get_jobs(self, timeout=5):
all_jobs = self.request_data('jobs') all_jobs = self.request_data('jobs', timeout=timeout)
sorted_jobs = [] if all_jobs is not None:
if all_jobs: sorted_jobs = []
for status_category in categories: for status_category in categories:
found_jobs = [x for x in all_jobs if x['status'] == status_category.value] found_jobs = [x for x in all_jobs if x['status'] == status_category.value]
if found_jobs: if found_jobs:
sorted_jobs.extend(found_jobs) sorted_jobs.extend(found_jobs)
return sorted_jobs all_jobs = sorted_jobs
return all_jobs
def get_data(self, timeout=5): def get_data(self, timeout=5):
all_data = self.request_data('full_status', timeout=timeout) all_data = self.request_data('full_status', timeout=timeout)

View File

@@ -16,6 +16,7 @@ import yaml
from flask import Flask, request, render_template, send_file, after_this_request, Response, redirect, url_for, abort from flask import Flask, request, render_template, send_file, after_this_request, Response, redirect, url_for, abort
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from lib.server.zeroconf_server import ZeroconfServer
from lib.render_queue import RenderQueue, JobNotFoundError from lib.render_queue import RenderQueue, JobNotFoundError
from lib.render_workers.worker_factory import RenderWorkerFactory from lib.render_workers.worker_factory import RenderWorkerFactory
from lib.render_workers.base_worker import string_to_status, RenderStatus 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}'") 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: if background_thread:
server_thread = threading.Thread( server_thread = threading.Thread(
target=lambda: server.run(host='0.0.0.0', port=RenderQueue.port, debug=False, use_reloader=False)) target=lambda: server.run(host='0.0.0.0', port=RenderQueue.port, debug=False, use_reloader=False))

View File

@@ -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()

View File

@@ -10,4 +10,5 @@ tkinterdnd2~=0.3.0
future==0.18.3 future==0.18.3
json2html~=1.3.0 json2html~=1.3.0
SQLAlchemy~=2.0.15 SQLAlchemy~=2.0.15
Pillow~=9.3.0 Pillow~=9.3.0
zeroconf~=0.63.0