Improve server shutdown (#126)

* Cleaned up server shutdown process

* Fix exception on shutdown in Windows
This commit is contained in:
2025-12-30 17:46:53 -06:00
committed by GitHub
parent f9b19587ba
commit e335328530
2 changed files with 71 additions and 68 deletions

View File

@@ -3,7 +3,7 @@ import logging
import threading import threading
from collections import deque from collections import deque
from server import start_server from server import ZordonServer
logger = logging.getLogger() logger = logging.getLogger()
@@ -13,6 +13,7 @@ def __setup_buffer_handler():
class BufferingHandler(logging.Handler, QObject): class BufferingHandler(logging.Handler, QObject):
new_record = pyqtSignal(str) new_record = pyqtSignal(str)
flushOnClose = True
def __init__(self, capacity=100): def __init__(self, capacity=100):
logging.Handler.__init__(self) logging.Handler.__init__(self)
@@ -52,12 +53,25 @@ def __show_gui(buffer_handler):
window.buffer_handler = buffer_handler window.buffer_handler = buffer_handler
window.show() window.show()
return app.exec() exit_code = app.exec()
# cleanup: remove and close the GUI logging handler before interpreter shutdown
root_logger = logging.getLogger()
if buffer_handler in root_logger.handlers:
root_logger.removeHandler(buffer_handler)
try:
buffer_handler.close()
except Exception:
# never let logging cleanup throw during shutdown
pass
return exit_code
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
local_server_thread = threading.Thread(target=start_server, args=[True], daemon=True) server = ZordonServer()
local_server_thread.start() server.start_server()
__show_gui(__setup_buffer_handler()) __show_gui(__setup_buffer_handler())
server.stop_server()
sys.exit() sys.exit()

107
server.py
View File

@@ -24,53 +24,15 @@ from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
logger = logging.getLogger() logger = logging.getLogger()
def start_server(skip_updates=False) -> int: class ZordonServer:
"""Initializes the application and runs it.
Args:
server_only: Run in server-only CLI mode. Default is False (runs in GUI mode).
Returns:
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
def __init__(self):
# 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)
# check for updates
if not skip_updates:
update_thread = threading.Thread(target=check_for_updates, args=(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME,
APP_VERSION))
update_thread.start()
# main start
logger.info(f"Starting {APP_NAME} Render Server")
return_code = 0
try:
# Load Config YAML # Load Config YAML
Config.setup_config_dir() Config.setup_config_dir()
Config.load_config(system_safe_path(os.path.join(Config.config_dir(), 'config.yaml'))) Config.load_config(system_safe_path(os.path.join(Config.config_dir(), 'config.yaml')))
@@ -88,6 +50,34 @@ def start_server(skip_updates=False) -> int:
logger.debug(f"Thumbs directory: {PreviewManager.storage_path}") logger.debug(f"Thumbs directory: {PreviewManager.storage_path}")
logger.debug(f"Engines directory: {EngineManager.engines_path}") logger.debug(f"Engines directory: {EngineManager.engines_path}")
self.api_server = None
def start_server(self):
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
# check for existing instance
existing_proc = existing_process(APP_NAME)
if existing_proc:
err_msg = f"Another instance of {APP_NAME} is already running (pid: {existing_proc.pid})"
logger.fatal(err_msg)
raise ProcessLookupError(err_msg)
# main start
logger.info(f"Starting {APP_NAME} Render Server")
# Set up the RenderQueue object # Set up the RenderQueue object
RenderQueue.load_state(database_directory=system_safe_path(os.path.expanduser(Config.upload_folder))) RenderQueue.load_state(database_directory=system_safe_path(os.path.expanduser(Config.upload_folder)))
ServerProxyManager.subscribe_to_listener() ServerProxyManager.subscribe_to_listener()
@@ -97,9 +87,9 @@ def start_server(skip_updates=False) -> int:
local_hostname = socket.gethostname() local_hostname = socket.gethostname()
# configure and start API server # configure and start API server
api_server = threading.Thread(target=start_api_server, args=(local_hostname,)) self.api_server = threading.Thread(target=start_api_server, args=(local_hostname,))
api_server.daemon = True self.api_server.daemon = True
api_server.start() self.api_server.start()
# start zeroconf server # start zeroconf server
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number) ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
@@ -115,27 +105,26 @@ def start_server(skip_updates=False) -> int:
logger.info(f"{APP_NAME} 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
# check for updates for render engines if configured or on first launch def is_running(self):
# if Config.update_engines_on_launch or not EngineManager.get_engines(): return self.api_server and self.api_server.is_alive()
# EngineManager.update_all_engines()
api_server.join() def stop_server(self):
logger.info(f"{APP_NAME} Render Server is preparing to stop")
except KeyboardInterrupt:
pass
except Exception as e:
logging.error(f"Unhandled exception: {e}")
return_code = 1
finally:
# shut down gracefully
logger.info(f"{APP_NAME} Render Server is preparing to shut down")
try: try:
ZeroconfServer.stop()
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()
logger.info(f"{APP_NAME} Render Server has shut down") logger.info(f"{APP_NAME} Render Server has shut down")
return sys.exit(return_code)
if __name__ == '__main__': if __name__ == '__main__':
start_server() server = ZordonServer()
try:
server.start_server()
server.api_server.join()
except KeyboardInterrupt:
pass
except Exception as e:
logger.error(f"Unhandled exception: {e}")
finally:
server.stop_server()