mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
Compare commits
17 Commits
v0.0.1-dev
...
feature/10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da03e30a2 | ||
|
|
4a566ec7c3 | ||
|
|
085d39fde8 | ||
|
|
d5f1224c33 | ||
|
|
e97e3d74c8 | ||
|
|
1af4169447 | ||
|
|
ea728f7809 | ||
|
|
a4e6fca73d | ||
|
|
9aafb5c0fb | ||
| 2548280dcc | |||
|
|
98ab837057 | ||
|
|
3fda87935e | ||
|
|
e35a5a689c | ||
| dea7574888 | |||
| a19db9fcf7 | |||
| 80b0adb2ad | |||
| 18873cec6f |
38
.github/workflows/create-executables.yml
vendored
Normal file
38
.github/workflows/create-executables.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Create Executables
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
- types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pyinstaller-build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Create Executables (Windows)
|
||||||
|
uses: sayyid5416/pyinstaller@v1
|
||||||
|
with:
|
||||||
|
python_ver: '3.11'
|
||||||
|
spec: 'main.spec'
|
||||||
|
requirements: 'requirements.txt'
|
||||||
|
upload_exe_with_name: 'Zordon'
|
||||||
|
pyinstaller-build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create Executables (Linux)
|
||||||
|
uses: sayyid5416/pyinstaller@v1
|
||||||
|
with:
|
||||||
|
python_ver: '3.11'
|
||||||
|
spec: 'main.spec'
|
||||||
|
requirements: 'requirements.txt'
|
||||||
|
upload_exe_with_name: 'Zordon'
|
||||||
|
pyinstaller-build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Create Executables (macOS)
|
||||||
|
uses: sayyid5416/pyinstaller@v1
|
||||||
|
with:
|
||||||
|
python_ver: '3.11'
|
||||||
|
spec: 'main.spec'
|
||||||
|
requirements: 'requirements.txt'
|
||||||
|
upload_exe_with_name: 'Zordon'
|
||||||
17
.github/workflows/pyinstaller.yml
vendored
17
.github/workflows/pyinstaller.yml
vendored
@@ -1,17 +0,0 @@
|
|||||||
name: Create Executable (Windows)
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
release:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pyinstaller-build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Create Executable (Windows)
|
|
||||||
uses: sayyid5416/pyinstaller@v1
|
|
||||||
with:
|
|
||||||
python_ver: '3.11'
|
|
||||||
spec: 'main.spec'
|
|
||||||
requirements: 'requirements.txt'
|
|
||||||
upload_exe_with_name: 'Zordon'
|
|
||||||
@@ -26,7 +26,7 @@ a = Analysis(
|
|||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=[],
|
excludes=[],
|
||||||
noarchive=False,
|
noarchive=False,
|
||||||
optimize=0,
|
optimize=1, # fyi: optim level 2 breaks on windows
|
||||||
)
|
)
|
||||||
pyz = PYZ(a.pure)
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ if platform.system() == 'Darwin': # macOS
|
|||||||
name=APP_NAME,
|
name=APP_NAME,
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=True,
|
||||||
upx=True,
|
upx=True,
|
||||||
console=False,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
@@ -87,7 +87,7 @@ elif platform.system() == 'Windows':
|
|||||||
name=APP_NAME,
|
name=APP_NAME,
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=True,
|
||||||
upx=True,
|
upx=True,
|
||||||
console=False,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
@@ -108,7 +108,7 @@ else: # linux
|
|||||||
name=APP_NAME,
|
name=APP_NAME,
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=True,
|
||||||
upx=True,
|
upx=True,
|
||||||
console=False,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
|
|||||||
@@ -50,8 +50,11 @@ def start_server(hostname=None):
|
|||||||
flask_log.setLevel(Config.flask_log_level.upper())
|
flask_log.setLevel(Config.flask_log_level.upper())
|
||||||
|
|
||||||
logger.debug('Starting API server')
|
logger.debug('Starting API server')
|
||||||
server.run(host='0.0.0.0', port=server.config['PORT'], debug=Config.flask_debug_enable, use_reloader=False,
|
try:
|
||||||
threaded=True)
|
server.run(host=hostname, port=server.config['PORT'], debug=Config.flask_debug_enable, use_reloader=False,
|
||||||
|
threaded=True)
|
||||||
|
finally:
|
||||||
|
logger.debug('Stopping API server')
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ from src.engines.blender.blender_engine import Blender
|
|||||||
from src.engines.core.base_downloader import EngineDownloader
|
from src.engines.core.base_downloader import EngineDownloader
|
||||||
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
||||||
|
|
||||||
# url = "https://download.blender.org/release/"
|
url = "https://download.blender.org/release/"
|
||||||
url = "https://ftp.nluug.nl/pub/graphics/blender/release/" # much faster mirror for testing
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ class EngineManager:
|
|||||||
def supported_engines():
|
def supported_engines():
|
||||||
return [Blender, FFMPEG]
|
return [Blender, FFMPEG]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def downloadable_engines(cls):
|
||||||
|
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def engine_with_name(cls, engine_name):
|
def engine_with_name(cls, engine_name):
|
||||||
for obj in cls.supported_engines():
|
for obj in cls.supported_engines():
|
||||||
@@ -30,7 +34,7 @@ class EngineManager:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_engines(cls, filter_name=None, include_corrupt=False):
|
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False):
|
||||||
|
|
||||||
if not cls.engines_path:
|
if not cls.engines_path:
|
||||||
raise FileNotFoundError("Engine path is not set")
|
raise FileNotFoundError("Engine path is not set")
|
||||||
@@ -92,46 +96,47 @@ class EngineManager:
|
|||||||
'type': 'system'
|
'type': 'system'
|
||||||
}
|
}
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
if not ignore_system:
|
||||||
futures = {
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
futures = {
|
||||||
for eng in cls.supported_engines()
|
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||||
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
|
for eng in cls.supported_engines()
|
||||||
}
|
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
|
||||||
|
}
|
||||||
|
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
result = future.result()
|
result = future.result()
|
||||||
if result:
|
if result:
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False):
|
def all_versions_for_engine(cls, engine_name, include_corrupt=False, ignore_system=False):
|
||||||
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt)
|
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt, ignore_system=ignore_system)
|
||||||
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
||||||
return sorted_versions
|
return sorted_versions
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def newest_engine_version(cls, engine, system_os=None, cpu=None):
|
def newest_engine_version(cls, engine, system_os=None, cpu=None, ignore_system=None):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.all_versions_for_engine(engine, ignore_system=ignore_system)
|
||||||
x['cpu'] == cpu]
|
if x['system_os'] == system_os and x['cpu'] == cpu]
|
||||||
return filtered[0]
|
return filtered[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None):
|
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None, ignore_system=False):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
filtered = [x for x in cls.get_engines(filter_name=engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.get_engines(filter_name=engine, ignore_system=ignore_system) if
|
||||||
x['cpu'] == cpu and x['version'] == version]
|
x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version]
|
||||||
return filtered[0] if filtered else False
|
return filtered[0] if filtered else False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -164,7 +169,7 @@ class EngineManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False):
|
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False, ignore_system=False):
|
||||||
|
|
||||||
engine_to_download = cls.engine_with_name(engine)
|
engine_to_download = cls.engine_with_name(engine)
|
||||||
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
||||||
@@ -187,7 +192,7 @@ class EngineManager:
|
|||||||
return thread
|
return thread
|
||||||
|
|
||||||
thread.join()
|
thread.join()
|
||||||
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu) # Check that engine downloaded
|
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded
|
||||||
if not found_engine:
|
if not found_engine:
|
||||||
logger.error(f"Error downloading {engine}")
|
logger.error(f"Error downloading {engine}")
|
||||||
return found_engine
|
return found_engine
|
||||||
@@ -213,31 +218,21 @@ class EngineManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_all_engines(cls):
|
def is_engine_update_available(cls, engine_class, ignore_system_installs=False):
|
||||||
def engine_update_task(engine_class):
|
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
latest_version = engine_class.downloader().find_most_recent_version()
|
||||||
latest_version = engine_class.downloader().find_most_recent_version()
|
|
||||||
|
|
||||||
if not latest_version:
|
if not latest_version:
|
||||||
logger.warning(f"Could not find most recent version of {engine.name()} to download")
|
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
|
||||||
return
|
return
|
||||||
|
|
||||||
version_num = latest_version.get('version')
|
version_num = latest_version.get('version')
|
||||||
if cls.is_version_downloaded(engine_class.name(), version_num):
|
if cls.is_version_downloaded(engine_class.name(), version_num, ignore_system=ignore_system_installs):
|
||||||
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||||
return
|
return
|
||||||
|
|
||||||
# download the engine
|
return latest_version
|
||||||
logger.info(f"Downloading latest version of {engine_class.name()} ({version_num})...")
|
|
||||||
cls.download_engine(engine=engine_class.name(), version=version_num, background=True)
|
|
||||||
|
|
||||||
logger.info(f"Checking for updates for render engines...")
|
|
||||||
threads = []
|
|
||||||
for engine in cls.supported_engines():
|
|
||||||
if engine.downloader():
|
|
||||||
thread = threading.Thread(target=engine_update_task, args=(engine,))
|
|
||||||
threads.append(thread)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
||||||
@@ -305,7 +300,8 @@ class EngineDownloadWorker(threading.Thread):
|
|||||||
self.cpu = cpu
|
self.cpu = cpu
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu)
|
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu,
|
||||||
|
ignore_system=True)
|
||||||
if existing_download:
|
if existing_download:
|
||||||
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
||||||
return existing_download
|
return existing_download
|
||||||
|
|||||||
59
src/init.py
59
src/init.py
@@ -5,6 +5,9 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QSettings
|
||||||
|
|
||||||
from src.api.api_server import start_server
|
from src.api.api_server import start_server
|
||||||
from src.api.preview_manager import PreviewManager
|
from src.api.preview_manager import PreviewManager
|
||||||
@@ -13,8 +16,10 @@ from src.distributed_job_manager import DistributedJobManager
|
|||||||
from src.engines.engine_manager import EngineManager
|
from src.engines.engine_manager import EngineManager
|
||||||
from src.render_queue import RenderQueue
|
from src.render_queue import RenderQueue
|
||||||
from src.utilities.config import Config
|
from src.utilities.config import Config
|
||||||
from src.utilities.misc_helper import system_safe_path, current_system_cpu, current_system_os, current_system_os_version
|
from src.utilities.misc_helper import (system_safe_path, current_system_cpu, current_system_os,
|
||||||
|
current_system_os_version, check_for_updates)
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
|
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER, APP_AUTHOR
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -29,16 +34,45 @@ def run(server_only=False) -> int:
|
|||||||
int: The exit status code.
|
int: The exit status code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def existing_process(process_name):
|
||||||
|
import psutil
|
||||||
|
current_pid = os.getpid()
|
||||||
|
current_process = psutil.Process(current_pid)
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'ppid']):
|
||||||
|
proc_name = proc.info['name'].lower().rstrip('.exe')
|
||||||
|
if proc_name == process_name.lower() and proc.info['pid'] != current_pid:
|
||||||
|
if proc.info['pid'] == current_process.ppid():
|
||||||
|
continue # parent process
|
||||||
|
elif proc.info['ppid'] == current_pid:
|
||||||
|
continue # child process
|
||||||
|
else:
|
||||||
|
return proc # unrelated process
|
||||||
|
return None
|
||||||
|
|
||||||
# setup logging
|
# setup logging
|
||||||
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
||||||
level=Config.server_log_level.upper())
|
level=Config.server_log_level.upper())
|
||||||
logging.getLogger("requests").setLevel(logging.WARNING) # suppress noisy requests/urllib3 logging
|
logging.getLogger("requests").setLevel(logging.WARNING) # suppress noisy requests/urllib3 logging
|
||||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# check for existing instance
|
||||||
|
existing_proc = existing_process(APP_NAME)
|
||||||
|
if existing_proc:
|
||||||
|
logger.fatal(f"Another instance of {APP_NAME} is already running (pid: {existing_proc.pid})")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Setup logging for console ui
|
# Setup logging for console ui
|
||||||
buffer_handler = __setup_buffer_handler() if not server_only else None
|
buffer_handler = __setup_buffer_handler() if not server_only else None
|
||||||
|
|
||||||
logger.info(f"Starting Zordon Render Server")
|
# check for updates
|
||||||
|
update_thread = threading.Thread(target=check_for_updates, args=(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME,
|
||||||
|
APP_VERSION))
|
||||||
|
update_thread.start()
|
||||||
|
|
||||||
|
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||||
|
|
||||||
|
# main start
|
||||||
|
logger.info(f"Starting {APP_NAME} Render Server")
|
||||||
return_code = 0
|
return_code = 0
|
||||||
try:
|
try:
|
||||||
# Load Config YAML
|
# Load Config YAML
|
||||||
@@ -63,9 +97,16 @@ def run(server_only=False) -> int:
|
|||||||
ServerProxyManager.subscribe_to_listener()
|
ServerProxyManager.subscribe_to_listener()
|
||||||
DistributedJobManager.subscribe_to_listener()
|
DistributedJobManager.subscribe_to_listener()
|
||||||
|
|
||||||
# check for updates for render engines if configured or on first launch
|
# check for updates for render engines if configured
|
||||||
if Config.update_engines_on_launch or not EngineManager.get_engines():
|
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||||
EngineManager.update_all_engines()
|
if settings.value('check_for_engine_updates_on_launch', False):
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
if settings.value(f'engine_download-{engine.name()}', False):
|
||||||
|
update_result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||||
|
EngineManager.download_engine(engine=engine.name(), version=update_result['version'],
|
||||||
|
background=True,
|
||||||
|
ignore_system=ignore_system)
|
||||||
|
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||||
|
|
||||||
# get hostname
|
# get hostname
|
||||||
local_hostname = socket.gethostname()
|
local_hostname = socket.gethostname()
|
||||||
@@ -77,13 +118,13 @@ def run(server_only=False) -> int:
|
|||||||
api_server.start()
|
api_server.start()
|
||||||
|
|
||||||
# start zeroconf server
|
# start zeroconf server
|
||||||
ZeroconfServer.configure("_zordon._tcp.local.", local_hostname, Config.port_number)
|
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
||||||
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
||||||
'system_cpu_cores': multiprocessing.cpu_count(),
|
'system_cpu_cores': multiprocessing.cpu_count(),
|
||||||
'system_os': current_system_os(),
|
'system_os': current_system_os(),
|
||||||
'system_os_version': current_system_os_version()}
|
'system_os_version': current_system_os_version()}
|
||||||
ZeroconfServer.start()
|
ZeroconfServer.start()
|
||||||
logger.info(f"Zordon Render Server started - Hostname: {local_hostname}")
|
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
||||||
RenderQueue.start() # Start evaluating the render queue
|
RenderQueue.start() # Start evaluating the render queue
|
||||||
|
|
||||||
# start in gui or server only (cli) mode
|
# start in gui or server only (cli) mode
|
||||||
@@ -100,13 +141,13 @@ def run(server_only=False) -> int:
|
|||||||
return_code = 1
|
return_code = 1
|
||||||
finally:
|
finally:
|
||||||
# shut down gracefully
|
# shut down gracefully
|
||||||
logger.info(f"Zordon Render Server is preparing to shut down")
|
logger.info(f"{APP_NAME} Render Server is preparing to shut down")
|
||||||
try:
|
try:
|
||||||
RenderQueue.prepare_for_shutdown()
|
RenderQueue.prepare_for_shutdown()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Exception during prepare for shutdown: {e}")
|
logger.exception(f"Exception during prepare for shutdown: {e}")
|
||||||
ZeroconfServer.stop()
|
ZeroconfServer.stop()
|
||||||
logger.info(f"Zordon Render Server has shut down")
|
logger.info(f"{APP_NAME} Render Server has shut down")
|
||||||
return sys.exit(return_code)
|
return sys.exit(return_code)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
481
src/ui/settings_window.py
Normal file
481
src/ui/settings_window.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import humanize
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from PyQt6 import QtCore
|
||||||
|
from PyQt6.QtCore import Qt, QSettings, pyqtSignal as Signal
|
||||||
|
from PyQt6.QtGui import QIcon
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QListWidget, QListWidgetItem, QStackedWidget, QVBoxLayout, \
|
||||||
|
QWidget, QLabel, QCheckBox, QLineEdit, \
|
||||||
|
QComboBox, QPushButton, QHBoxLayout, QGroupBox, QTableWidget, QAbstractItemView, QTableWidgetItem, QHeaderView, \
|
||||||
|
QMessageBox
|
||||||
|
|
||||||
|
from api.server_proxy import RenderServerProxy
|
||||||
|
from engines.engine_manager import EngineManager
|
||||||
|
from utilities.config import Config
|
||||||
|
from utilities.misc_helper import launch_url, system_safe_path
|
||||||
|
from version import APP_AUTHOR, APP_NAME
|
||||||
|
|
||||||
|
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if not EngineManager.engines_path: # fix issue where sometimes path was not set
|
||||||
|
EngineManager.engines_path = system_safe_path(
|
||||||
|
os.path.join(os.path.join(os.path.expanduser(Config.upload_folder),
|
||||||
|
'engines')))
|
||||||
|
|
||||||
|
self.installed_engines_table = None
|
||||||
|
|
||||||
|
self.setWindowTitle("Settings")
|
||||||
|
|
||||||
|
# Create the main layout
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Create the sidebar (QListWidget) for navigation
|
||||||
|
self.sidebar = QListWidget()
|
||||||
|
self.sidebar.setFixedWidth(150)
|
||||||
|
|
||||||
|
# Set the icon size
|
||||||
|
self.sidebar.setIconSize(QtCore.QSize(32, 32)) # Increase the icon size to 32x32 pixels
|
||||||
|
|
||||||
|
# Adjust the font size for the sidebar items
|
||||||
|
font = self.sidebar.font()
|
||||||
|
font.setPointSize(12) # Increase the font size
|
||||||
|
self.sidebar.setFont(font)
|
||||||
|
|
||||||
|
# Add items with icons to the sidebar
|
||||||
|
resources_dir = os.path.join(Path(__file__).resolve().parent.parent.parent, 'resources')
|
||||||
|
self.add_sidebar_item("General", os.path.join(resources_dir, "Gear.png"))
|
||||||
|
self.add_sidebar_item("Server", os.path.join(resources_dir, "Server.png"))
|
||||||
|
self.add_sidebar_item("Engines", os.path.join(resources_dir, "Blender.png"))
|
||||||
|
self.sidebar.setCurrentRow(0)
|
||||||
|
|
||||||
|
# Create the stacked widget to hold different settings pages
|
||||||
|
self.stacked_widget = QStackedWidget()
|
||||||
|
|
||||||
|
# Create pages for each section
|
||||||
|
general_page = self.create_general_page()
|
||||||
|
network_page = self.create_network_page()
|
||||||
|
engines_page = self.create_engines_page()
|
||||||
|
|
||||||
|
# Add pages to the stacked widget
|
||||||
|
self.stacked_widget.addWidget(general_page)
|
||||||
|
self.stacked_widget.addWidget(network_page)
|
||||||
|
self.stacked_widget.addWidget(engines_page)
|
||||||
|
|
||||||
|
# Connect the sidebar to the stacked widget
|
||||||
|
self.sidebar.currentRowChanged.connect(self.stacked_widget.setCurrentIndex)
|
||||||
|
|
||||||
|
# Create a horizontal layout to hold the sidebar and stacked widget
|
||||||
|
content_layout = QHBoxLayout()
|
||||||
|
content_layout.addWidget(self.sidebar)
|
||||||
|
content_layout.addWidget(self.stacked_widget)
|
||||||
|
|
||||||
|
# Add the content layout to the main layout
|
||||||
|
main_layout.addLayout(content_layout)
|
||||||
|
|
||||||
|
# Add the "OK" button at the bottom
|
||||||
|
ok_button = QPushButton("OK")
|
||||||
|
ok_button.clicked.connect(self.close)
|
||||||
|
ok_button.setFixedWidth(80)
|
||||||
|
ok_button.setDefault(True)
|
||||||
|
main_layout.addWidget(ok_button, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
|
||||||
|
# Create a central widget and set the layout
|
||||||
|
central_widget = QWidget()
|
||||||
|
central_widget.setLayout(main_layout)
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
self.setMinimumSize(700, 400)
|
||||||
|
|
||||||
|
def add_sidebar_item(self, name, icon_path):
|
||||||
|
"""Add an item with an icon to the sidebar."""
|
||||||
|
item = QListWidgetItem(QIcon(icon_path), name)
|
||||||
|
self.sidebar.addItem(item)
|
||||||
|
|
||||||
|
def create_general_page(self):
|
||||||
|
"""Create the General settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Startup Settings Group
|
||||||
|
startup_group = QGroupBox("Startup Settings")
|
||||||
|
startup_layout = QVBoxLayout()
|
||||||
|
# startup_layout.addWidget(QCheckBox("Start application on system startup"))
|
||||||
|
check_for_updates_checkbox = QCheckBox("Check for updates automatically")
|
||||||
|
check_for_updates_checkbox.setChecked(settings.value("auto_check_for_updates", True))
|
||||||
|
check_for_updates_checkbox.stateChanged.connect(lambda state: settings.setValue("auto_check_for_updates", bool(state)))
|
||||||
|
startup_layout.addWidget(check_for_updates_checkbox)
|
||||||
|
startup_group.setLayout(startup_layout)
|
||||||
|
|
||||||
|
# Local Files Group
|
||||||
|
data_path = os.path.expanduser(Config.upload_folder)
|
||||||
|
path_size = sum(f.stat().st_size for f in Path(data_path).rglob('*') if f.is_file())
|
||||||
|
database_group = QGroupBox("Local Files")
|
||||||
|
database_layout = QVBoxLayout()
|
||||||
|
database_layout.addWidget(QLabel(f"Local Directory: {data_path}"))
|
||||||
|
database_layout.addWidget(QLabel(f"Size: {humanize.naturalsize(path_size, binary=True)}"))
|
||||||
|
open_database_path_button = QPushButton("Open Directory")
|
||||||
|
open_database_path_button.clicked.connect(lambda: launch_url(data_path))
|
||||||
|
open_database_path_button.setFixedWidth(200)
|
||||||
|
database_layout.addWidget(open_database_path_button)
|
||||||
|
database_group.setLayout(database_layout)
|
||||||
|
|
||||||
|
|
||||||
|
# Render Settings Group
|
||||||
|
render_settings_group = QGroupBox("Render Settings")
|
||||||
|
render_settings_layout = QVBoxLayout()
|
||||||
|
render_settings_layout.addWidget(QLabel("Restrict to render nodes with same:"))
|
||||||
|
require_same_engine_checkbox = QCheckBox("Renderer Version")
|
||||||
|
require_same_engine_checkbox.setChecked(settings.value("render_require_same_engine_version"))
|
||||||
|
require_same_engine_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_engine_version", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_engine_checkbox)
|
||||||
|
require_same_cpu_checkbox = QCheckBox("CPU Architecture")
|
||||||
|
require_same_cpu_checkbox.setChecked(settings.value("render_require_same_cpu_type"))
|
||||||
|
require_same_cpu_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_cpu_type", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_cpu_checkbox)
|
||||||
|
require_same_os_checkbox = QCheckBox("Operating System")
|
||||||
|
require_same_os_checkbox.setChecked(settings.value("render_require_same_os"))
|
||||||
|
require_same_os_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_os", bool(state)))
|
||||||
|
render_settings_layout.addWidget(require_same_os_checkbox)
|
||||||
|
render_settings_group.setLayout(render_settings_layout)
|
||||||
|
|
||||||
|
layout.addWidget(startup_group)
|
||||||
|
layout.addWidget(database_group)
|
||||||
|
layout.addWidget(render_settings_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def create_network_page(self):
|
||||||
|
"""Create the Network settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Sharing Settings Group
|
||||||
|
sharing_group = QGroupBox("Sharing Settings")
|
||||||
|
sharing_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
enable_sharing_checkbox = QCheckBox("Enable other computers on the network to render to this machine")
|
||||||
|
enable_sharing_checkbox.setChecked(settings.value("enable_network_sharing", False))
|
||||||
|
enable_sharing_checkbox.stateChanged.connect(self.toggle_render_sharing)
|
||||||
|
sharing_layout.addWidget(enable_sharing_checkbox)
|
||||||
|
|
||||||
|
password_layout = QHBoxLayout()
|
||||||
|
password_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.enable_network_password_checkbox = QCheckBox("Enable network password:")
|
||||||
|
self.enable_network_password_checkbox.setChecked(settings.value("enable_network_password", False))
|
||||||
|
self.enable_network_password_checkbox.stateChanged.connect(self.enable_network_password_changed)
|
||||||
|
sharing_layout.addWidget(self.enable_network_password_checkbox)
|
||||||
|
self.network_password_line = QLineEdit()
|
||||||
|
self.network_password_line.setPlaceholderText("Enter a password")
|
||||||
|
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.network_password_line.setEnabled(settings.value("enable_network_password", False))
|
||||||
|
password_layout.addWidget(self.network_password_line)
|
||||||
|
self.show_password_button = QPushButton("Show")
|
||||||
|
self.show_password_button.setEnabled(settings.value("enable_network_password", False))
|
||||||
|
self.show_password_button.clicked.connect(self.show_password_button_pressed)
|
||||||
|
password_layout.addWidget(self.show_password_button)
|
||||||
|
sharing_layout.addLayout(password_layout)
|
||||||
|
|
||||||
|
sharing_group.setLayout(sharing_layout)
|
||||||
|
|
||||||
|
layout.addWidget(sharing_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def toggle_render_sharing(self, enable_sharing):
|
||||||
|
settings.setValue("enable_network_sharing", enable_sharing)
|
||||||
|
self.enable_network_password_checkbox.setEnabled(enable_sharing)
|
||||||
|
enable_password = enable_sharing and settings.value("enable_network_password", False)
|
||||||
|
self.network_password_line.setEnabled(enable_password)
|
||||||
|
self.show_password_button.setEnabled(enable_password)
|
||||||
|
|
||||||
|
def enable_network_password_changed(self, new_value):
|
||||||
|
settings.setValue("enable_network_password", new_value)
|
||||||
|
self.network_password_line.setEnabled(new_value)
|
||||||
|
self.show_password_button.setEnabled(new_value)
|
||||||
|
|
||||||
|
def show_password_button_pressed(self):
|
||||||
|
# toggle showing / hiding the password
|
||||||
|
show_pass = self.show_password_button.text() == "Show"
|
||||||
|
self.show_password_button.setText("Hide" if show_pass else "Show")
|
||||||
|
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Normal if show_pass else QLineEdit.EchoMode.Normal)
|
||||||
|
|
||||||
|
def create_engines_page(self):
|
||||||
|
"""Create the Engines settings page."""
|
||||||
|
page = QWidget()
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Installed Engines Group
|
||||||
|
installed_group = QGroupBox("Installed Engines")
|
||||||
|
installed_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Setup table
|
||||||
|
self.installed_engines_table = EngineTableWidget()
|
||||||
|
self.installed_engines_table.row_selected.connect(self.engine_table_selected)
|
||||||
|
installed_layout.addWidget(self.installed_engines_table)
|
||||||
|
|
||||||
|
# Ignore system installs
|
||||||
|
engine_ignore_system_installs_checkbox = QCheckBox("Ignore system installs")
|
||||||
|
engine_ignore_system_installs_checkbox.setChecked(settings.value("engines_ignore_system_installs", False))
|
||||||
|
engine_ignore_system_installs_checkbox.stateChanged.connect(self.change_ignore_system_installs)
|
||||||
|
installed_layout.addWidget(engine_ignore_system_installs_checkbox)
|
||||||
|
|
||||||
|
# Engine Launch / Delete buttons
|
||||||
|
installed_buttons_layout = QHBoxLayout()
|
||||||
|
self.launch_engine_button = QPushButton("Launch")
|
||||||
|
self.launch_engine_button.setEnabled(False)
|
||||||
|
self.launch_engine_button.clicked.connect(self.launch_selected_engine)
|
||||||
|
self.delete_engine_button = QPushButton("Delete")
|
||||||
|
self.delete_engine_button.setEnabled(False)
|
||||||
|
self.delete_engine_button.clicked.connect(self.delete_selected_engine)
|
||||||
|
|
||||||
|
installed_buttons_layout.addWidget(self.launch_engine_button)
|
||||||
|
installed_buttons_layout.addWidget(self.delete_engine_button)
|
||||||
|
installed_layout.addLayout(installed_buttons_layout)
|
||||||
|
installed_group.setLayout(installed_layout)
|
||||||
|
|
||||||
|
# Engine Updates Group
|
||||||
|
engine_updates_group = QGroupBox("Auto-Install")
|
||||||
|
engine_updates_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
engine_download_layout = QHBoxLayout()
|
||||||
|
engine_download_layout.addWidget(QLabel("Enable Downloads for:"))
|
||||||
|
|
||||||
|
at_least_one_downloadable = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
engine_download_check = QCheckBox(engine.name())
|
||||||
|
is_checked = settings.value(f"engine_download-{engine.name()}", False)
|
||||||
|
at_least_one_downloadable |= is_checked
|
||||||
|
engine_download_check.setChecked(is_checked)
|
||||||
|
# Capture the checkbox correctly using a default argument in lambda
|
||||||
|
engine_download_check.clicked.connect(
|
||||||
|
lambda state, checkbox=engine_download_check: self.engine_download_settings_changed(state, checkbox.text())
|
||||||
|
)
|
||||||
|
engine_download_layout.addWidget(engine_download_check)
|
||||||
|
|
||||||
|
engine_updates_layout.addLayout(engine_download_layout)
|
||||||
|
|
||||||
|
check_for_engine_updates_checkbox = QCheckBox("Check for new versions on launch")
|
||||||
|
check_for_engine_updates_checkbox.setChecked(settings.value('check_for_engine_updates_on_launch', True))
|
||||||
|
check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||||
|
check_for_engine_updates_checkbox.stateChanged.connect(
|
||||||
|
lambda state: settings.setValue("check_for_engine_updates_on_launch", bool(state)))
|
||||||
|
engine_updates_layout.addWidget(check_for_engine_updates_checkbox)
|
||||||
|
self.engines_last_update_label = QLabel()
|
||||||
|
self.update_last_checked_label()
|
||||||
|
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||||
|
engine_updates_layout.addWidget(self.engines_last_update_label)
|
||||||
|
self.check_for_new_engines_button = QPushButton("Check for New Versions...")
|
||||||
|
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||||
|
self.check_for_new_engines_button.clicked.connect(self.check_for_new_engines)
|
||||||
|
engine_updates_layout.addWidget(self.check_for_new_engines_button)
|
||||||
|
engine_updates_group.setLayout(engine_updates_layout)
|
||||||
|
|
||||||
|
layout.addWidget(installed_group)
|
||||||
|
layout.addWidget(engine_updates_group)
|
||||||
|
|
||||||
|
layout.addStretch() # Add a stretch to push content to the top
|
||||||
|
page.setLayout(layout)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def change_ignore_system_installs(self, value):
|
||||||
|
settings.setValue("engines_ignore_system_installs", bool(value))
|
||||||
|
self.installed_engines_table.update_table()
|
||||||
|
|
||||||
|
def update_last_checked_label(self):
|
||||||
|
"""Retrieve the last check timestamp and return a human-friendly string."""
|
||||||
|
last_checked_str = settings.value("engines_last_update_time", None)
|
||||||
|
if not last_checked_str:
|
||||||
|
time_string = "Never"
|
||||||
|
else:
|
||||||
|
last_checked_dt = datetime.fromisoformat(last_checked_str)
|
||||||
|
now = datetime.now()
|
||||||
|
time_string = humanize.naturaltime(now - last_checked_dt)
|
||||||
|
self.engines_last_update_label.setText(f"Last Updated: {time_string}")
|
||||||
|
|
||||||
|
def engine_download_settings_changed(self, state, engine_name):
|
||||||
|
settings.setValue(f"engine_download-{engine_name}", state)
|
||||||
|
|
||||||
|
at_least_one_downloadable = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
at_least_one_downloadable |= settings.value(f"engine_download-{engine.name()}", False)
|
||||||
|
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||||
|
self.check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||||
|
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||||
|
|
||||||
|
def delete_selected_engine(self):
|
||||||
|
engine_info = self.installed_engines_table.selected_engine_data()
|
||||||
|
reply = QMessageBox.question(self, f"Delete {engine_info['engine']} {engine_info['version']}?",
|
||||||
|
f"Do you want to delete {engine_info['engine']} {engine_info['version']}?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
|
||||||
|
if reply is not QMessageBox.StandardButton.Yes:
|
||||||
|
return
|
||||||
|
|
||||||
|
delete_result = EngineManager.delete_engine_download(engine_info.get('engine'),
|
||||||
|
engine_info.get('version'),
|
||||||
|
engine_info.get('system_os'),
|
||||||
|
engine_info.get('cpu'))
|
||||||
|
|
||||||
|
if delete_result:
|
||||||
|
QMessageBox.information(self, f"{engine_info['engine']} {engine_info['version']} Deleted",
|
||||||
|
f"{engine_info['engine']} {engine_info['version']} deleted successfully",
|
||||||
|
QMessageBox.StandardButton.Ok)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, f"Unknown Error",
|
||||||
|
f"Unknown error while deleting {engine_info['engine']} {engine_info['version']}.",
|
||||||
|
QMessageBox.StandardButton.Ok)
|
||||||
|
|
||||||
|
self.installed_engines_table.update_table(use_cached=False)
|
||||||
|
|
||||||
|
def launch_selected_engine(self):
|
||||||
|
engine_info = self.installed_engines_table.selected_engine_data()
|
||||||
|
if engine_info:
|
||||||
|
launch_url(engine_info['path'])
|
||||||
|
|
||||||
|
def engine_table_selected(self):
|
||||||
|
engine_data = self.installed_engines_table.selected_engine_data()
|
||||||
|
if engine_data:
|
||||||
|
self.launch_engine_button.setEnabled(bool(engine_data.get('path') or True))
|
||||||
|
self.delete_engine_button.setEnabled(engine_data.get('type') == 'managed')
|
||||||
|
else:
|
||||||
|
self.launch_engine_button.setEnabled(False)
|
||||||
|
self.delete_engine_button.setEnabled(False)
|
||||||
|
|
||||||
|
def check_for_new_engines(self):
|
||||||
|
|
||||||
|
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||||
|
messagebox_shown = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
if settings.value(f'engine_download-{engine.name()}', False):
|
||||||
|
result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||||
|
if result:
|
||||||
|
result['name'] = engine.name()
|
||||||
|
msg_box = QMessageBox()
|
||||||
|
msg_box.setWindowTitle(f"{result['name']} ({result['version']}) Available")
|
||||||
|
msg_box.setText(f"A new version of {result['name']} is available ({result['version']}).\n\n"
|
||||||
|
f"Would you like to download it now?")
|
||||||
|
msg_box.setIcon(QMessageBox.Icon.Question)
|
||||||
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
msg_result = msg_box.exec()
|
||||||
|
messagebox_shown = True
|
||||||
|
if msg_result == QMessageBox.StandardButton.Yes:
|
||||||
|
EngineManager.download_engine(engine=engine.name(), version=result['version'], background=True,
|
||||||
|
ignore_system=ignore_system)
|
||||||
|
self.update_engine_download_status()
|
||||||
|
|
||||||
|
if not messagebox_shown:
|
||||||
|
msg_box = QMessageBox()
|
||||||
|
msg_box.setWindowTitle("No Updates Available")
|
||||||
|
msg_box.setText("All your render engines are up-to-date.")
|
||||||
|
msg_box.setIcon(QMessageBox.Icon.Information)
|
||||||
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||||
|
msg_box.exec()
|
||||||
|
|
||||||
|
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||||
|
self.update_engine_download_status()
|
||||||
|
|
||||||
|
def update_engine_download_status(self):
|
||||||
|
running_tasks = [x for x in EngineManager.download_tasks if x.is_alive()]
|
||||||
|
if not running_tasks:
|
||||||
|
self.update_last_checked_label()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.engines_last_update_label.setText(f"Downloading {running_tasks[0].engine} ({running_tasks[0].version})...")
|
||||||
|
|
||||||
|
|
||||||
|
class EngineTableWidget(QWidget):
|
||||||
|
row_selected = Signal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.table = QTableWidget(0, 4)
|
||||||
|
self.table.setHorizontalHeaderLabels(["Engine", "Version", "Type", "Path"])
|
||||||
|
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.table.verticalHeader().setVisible(False)
|
||||||
|
# self.table_widget.itemSelectionChanged.connect(self.engine_picked)
|
||||||
|
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
self.table.selectionModel().selectionChanged.connect(self.on_selection_changed)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
|
self.raw_server_data = None
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
"""Runs when the widget is about to be shown."""
|
||||||
|
self.update_table()
|
||||||
|
super().showEvent(event) # Ensure normal event processing
|
||||||
|
|
||||||
|
def update_table(self, use_cached=True):
|
||||||
|
if not self.raw_server_data or not use_cached:
|
||||||
|
self.raw_server_data = RenderServerProxy(socket.gethostname()).get_renderer_info()
|
||||||
|
if not self.raw_server_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
table_data = [] # convert the data into a flat list
|
||||||
|
for _, engine_data in self.raw_server_data.items():
|
||||||
|
table_data.extend(engine_data['versions'])
|
||||||
|
|
||||||
|
if settings.value("engines_ignore_system_installs", False):
|
||||||
|
table_data = [x for x in table_data if x['type'] != 'system']
|
||||||
|
|
||||||
|
self.table.clear()
|
||||||
|
self.table.setRowCount(len(table_data))
|
||||||
|
self.table.setColumnCount(4)
|
||||||
|
|
||||||
|
self.table.setHorizontalHeaderLabels(['Engine', 'Version', 'Type', 'Path'])
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||||
|
|
||||||
|
for row, engine in enumerate(table_data):
|
||||||
|
self.table.setItem(row, 0, QTableWidgetItem(engine['engine']))
|
||||||
|
self.table.setItem(row, 1, QTableWidgetItem(engine['version']))
|
||||||
|
self.table.setItem(row, 2, QTableWidgetItem(engine['type']))
|
||||||
|
self.table.setItem(row, 3, QTableWidgetItem(engine['path']))
|
||||||
|
|
||||||
|
self.table.selectRow(0)
|
||||||
|
|
||||||
|
def selected_engine_data(self):
|
||||||
|
"""Returns the data from the selected row as a dictionary."""
|
||||||
|
row = self.table.currentRow() # Get the selected row index
|
||||||
|
|
||||||
|
if row < 0 or not len(self.table.selectedItems()): # No row selected
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"engine": self.table.item(row, 0).text(),
|
||||||
|
"version": self.table.item(row, 1).text(),
|
||||||
|
"type": self.table.item(row, 2).text(),
|
||||||
|
"path": self.table.item(row, 3).text(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def on_selection_changed(self):
|
||||||
|
self.row_selected.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication([])
|
||||||
|
window = SettingsWindow()
|
||||||
|
window.show()
|
||||||
|
app.exec()
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
''' app/ui/widgets/menubar.py '''
|
''' app/ui/widgets/menubar.py '''
|
||||||
import sys
|
|
||||||
|
|
||||||
from PyQt6.QtGui import QAction
|
from PyQt6.QtGui import QAction
|
||||||
from PyQt6.QtWidgets import QMenuBar, QApplication
|
from PyQt6.QtWidgets import QMenuBar, QApplication, QMessageBox, QDialog, QVBoxLayout, QLabel, QPushButton
|
||||||
|
|
||||||
|
|
||||||
class MenuBar(QMenuBar):
|
class MenuBar(QMenuBar):
|
||||||
@@ -16,6 +14,8 @@ class MenuBar(QMenuBar):
|
|||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.settings_window = None
|
||||||
|
|
||||||
# setup menus
|
# setup menus
|
||||||
file_menu = self.addMenu("File")
|
file_menu = self.addMenu("File")
|
||||||
# edit_menu = self.addMenu("Edit")
|
# edit_menu = self.addMenu("Edit")
|
||||||
@@ -32,7 +32,7 @@ class MenuBar(QMenuBar):
|
|||||||
settings_action = QAction("Settings...", self)
|
settings_action = QAction("Settings...", self)
|
||||||
settings_action.triggered.connect(self.show_settings)
|
settings_action.triggered.connect(self.show_settings)
|
||||||
settings_action.setShortcut(f'Ctrl+,')
|
settings_action.setShortcut(f'Ctrl+,')
|
||||||
# file_menu.addAction(settings_action) # todo: enable once we have a setting screen
|
file_menu.addAction(settings_action)
|
||||||
# exit
|
# exit
|
||||||
exit_action = QAction('&Exit', self)
|
exit_action = QAction('&Exit', self)
|
||||||
exit_action.setShortcut('Ctrl+Q')
|
exit_action.setShortcut('Ctrl+Q')
|
||||||
@@ -43,15 +43,63 @@ class MenuBar(QMenuBar):
|
|||||||
about_action = QAction("About", self)
|
about_action = QAction("About", self)
|
||||||
about_action.triggered.connect(self.show_about)
|
about_action.triggered.connect(self.show_about)
|
||||||
help_menu.addAction(about_action)
|
help_menu.addAction(about_action)
|
||||||
|
update_action = QAction("Check for Updates...", self)
|
||||||
|
update_action.triggered.connect(self.check_for_updates)
|
||||||
|
help_menu.addAction(update_action)
|
||||||
|
|
||||||
def new_job(self):
|
def new_job(self):
|
||||||
self.parent().new_job()
|
self.parent().new_job()
|
||||||
|
|
||||||
def show_settings(self):
|
def show_settings(self):
|
||||||
pass
|
from src.ui.settings_window import SettingsWindow
|
||||||
|
self.settings_window = SettingsWindow()
|
||||||
|
self.settings_window.show()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_about():
|
def show_about():
|
||||||
from src.ui.about_window import AboutDialog
|
from src.ui.about_window import AboutDialog
|
||||||
dialog = AboutDialog()
|
dialog = AboutDialog()
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_for_updates():
|
||||||
|
from src.utilities.misc_helper import check_for_updates
|
||||||
|
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
||||||
|
found_update = check_for_updates(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME, APP_VERSION)
|
||||||
|
if found_update:
|
||||||
|
dialog = UpdateDialog(found_update, APP_VERSION)
|
||||||
|
dialog.exec()
|
||||||
|
else:
|
||||||
|
QMessageBox.information(None, "No Update", "No updates available.")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDialog(QDialog):
|
||||||
|
def __init__(self, release_info, current_version, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle(f"Update Available ({current_version} -> {release_info['tag_name']})")
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
label = QLabel(f"A new version ({release_info['tag_name']}) is available! Current version: {current_version}")
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
# Label to show the release notes
|
||||||
|
description = QLabel(release_info["body"])
|
||||||
|
layout.addWidget(description)
|
||||||
|
|
||||||
|
# Button to download the latest version
|
||||||
|
download_button = QPushButton(f"Download Latest Version ({release_info['tag_name']})")
|
||||||
|
download_button.clicked.connect(lambda: self.open_url(release_info["html_url"]))
|
||||||
|
layout.addWidget(download_button)
|
||||||
|
|
||||||
|
# OK button to dismiss the dialog
|
||||||
|
ok_button = QPushButton("Dismiss")
|
||||||
|
ok_button.clicked.connect(self.accept) # Close the dialog when clicked
|
||||||
|
layout.addWidget(ok_button)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def open_url(self, url):
|
||||||
|
from PyQt6.QtCore import QUrl
|
||||||
|
from PyQt6.QtGui import QDesktopServices
|
||||||
|
QDesktopServices.openUrl(QUrl(url))
|
||||||
|
self.accept()
|
||||||
|
|||||||
@@ -159,6 +159,33 @@ def copy_directory_contents(src_dir, dst_dir):
|
|||||||
shutil.copy2(src_path, dst_path)
|
shutil.copy2(src_path, dst_path)
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_updates(repo_name, repo_owner, app_name, current_version):
|
||||||
|
def get_github_releases(owner, repo):
|
||||||
|
import requests
|
||||||
|
url = f"https://api.github.com/repos/{owner}/{repo}/releases"
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=3)
|
||||||
|
response.raise_for_status()
|
||||||
|
releases = response.json()
|
||||||
|
return releases
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking for updates: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
releases = get_github_releases(repo_owner, repo_name)
|
||||||
|
if not releases:
|
||||||
|
return
|
||||||
|
|
||||||
|
latest_version = releases[0]
|
||||||
|
latest_version_tag = latest_version['tag_name']
|
||||||
|
|
||||||
|
from packaging import version
|
||||||
|
if version.parse(latest_version_tag) > version.parse(current_version):
|
||||||
|
logger.info(f"Newer version of {app_name} available. "
|
||||||
|
f"Latest: {latest_version_tag}, Current: {current_version}")
|
||||||
|
return latest_version
|
||||||
|
|
||||||
|
|
||||||
def is_localhost(comparison_hostname):
|
def is_localhost(comparison_hostname):
|
||||||
# this is necessary because socket.gethostname() does not always include '.local' - This is a sanitized comparison
|
# this is necessary because socket.gethostname() does not always include '.local' - This is a sanitized comparison
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ APP_AUTHOR = "Brett Williams"
|
|||||||
APP_DESCRIPTION = "Distributed Render Farm Tools"
|
APP_DESCRIPTION = "Distributed Render Farm Tools"
|
||||||
APP_COPYRIGHT_YEAR = "2024"
|
APP_COPYRIGHT_YEAR = "2024"
|
||||||
APP_LICENSE = "MIT License"
|
APP_LICENSE = "MIT License"
|
||||||
|
APP_REPO_NAME = APP_NAME
|
||||||
|
APP_REPO_OWNER = "blw1138"
|
||||||
|
|||||||
Reference in New Issue
Block a user