From 8d80b5f6d99ee144535d5cfca045adc5c889a378 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 17 Jun 2023 13:44:55 -0400 Subject: [PATCH] add server class --- installer.py | 5 ++- javascript/style.css | 6 +-- launch.py | 4 +- modules/api/api.py | 52 +++++++--------------- modules/sd_models.py | 1 - modules/server.py | 85 ++++++++++++++++++++++++++++++++++++ modules/ui.py | 8 ++-- modules/ui_extra_networks.py | 12 ++--- webui.py | 5 ++- 9 files changed, 124 insertions(+), 54 deletions(-) create mode 100644 modules/server.py diff --git a/installer.py b/installer.py index f6bcda06c..ea8aca055 100644 --- a/installer.py +++ b/installer.py @@ -66,7 +66,10 @@ def setup_logging(clean=False): "inspect.value.border": "black", })) level = logging.DEBUG if args.debug else logging.INFO - logging.basicConfig(level=logging.ERROR, format='%(asctime)s | %(name)s | %(levelname)s | %(module)s | %(message)s', filename=log_file, filemode='a', encoding='utf-8', force=True) + try: + logging.basicConfig(level=logging.ERROR, format='%(asctime)s | %(name)s | %(levelname)s | %(module)s | %(message)s', filename=log_file, filemode='a', encoding='utf-8', force=True) + except Exception: + logging.basicConfig(level=logging.ERROR, format='%(asctime)s | %(name)s | %(levelname)s | %(module)s | %(message)s') # to be able to report unsupported python version log.setLevel(logging.DEBUG) # log to file is always at level debug for facility `sd` pretty_install(console=console) traceback_install(console=console, extra_lines=1, width=console.width, word_wrap=False, indent_guides=False, suppress=[]) diff --git a/javascript/style.css b/javascript/style.css index b78c4da44..a80922445 100644 --- a/javascript/style.css +++ b/javascript/style.css @@ -566,9 +566,9 @@ table.settings-value-table td{ /* extra networks */ .extra-networks > div > [id *= '_extra_']{ margin: 0.3em; } -.extra-network-subdirs{ padding: 0.2em 0.35em; } -.extra-network-subdirs button{ margin: 0 0.15em; } -.extra-networks .tab-nav .search{ display: inline-block; max-width: 16em; margin: 0.3em; align-self: center; width: 16em; } +.extra-network-subdirs { padding: 0.2em 0.35em; } +.extra-network-subdirs button { margin: 0 0.15em; box-shadow: none; } +.extra-networks .tab-nav .search { display: inline-block; max-width: 16em; margin: 0.3em; align-self: center; width: 16em; } #txt2img_extra_view, #img2img_extra_view { width: auto; } .extra-network-cards .nocards, .extra-network-thumbs .nocards{ margin: 1.25em 0.5em 0.5em 0.5em; } .extra-network-cards .nocards h1, .extra-network-thumbs .nocards h1{ font-size: 1.5em; margin-bottom: 1em; } diff --git a/launch.py b/launch.py index 5ca0b1ccd..0f8ccaabe 100644 --- a/launch.py +++ b/launch.py @@ -191,10 +191,12 @@ if __name__ == "__main__": while True: try: alive = instance.thread.is_alive() + requests = instance.server_state.total_requests if hasattr(instance, 'server_state') else 0 except Exception: alive = False + requests = 0 if round(time.time()) % 120 == 0: - installer.log.debug(f'Server alive: {alive} Memory {get_memory_stats()}') + installer.log.debug(f'Server alive={alive} Requests={requests} memory {get_memory_stats()} ') if not alive: if instance.wants_restart: installer.log.info('Server restarting...') diff --git a/modules/api/api.py b/modules/api/api.py index 4d83d6f3a..57c808389 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -1,7 +1,6 @@ import io import time import base64 -import logging from io import BytesIO from typing import List, Dict, Any from threading import Lock @@ -637,40 +636,19 @@ class Api: cuda = { 'error': f'{err}' } return models.MemoryResponse(ram = ram, cuda = cuda) - def launch_uvicorn(self): - self.app.include_router(self.router) - import uvicorn - config: uvicorn.Config = { - "host": "0.0.0.0" if shared.cmd_opts.listen else "127.0.0.1", - "port": shared.cmd_opts.port if shared.cmd_opts.port else 7861, - "loop": "auto", # auto, asyncio, uvloop - "http": "auto", # auto, h11, httptools - "interface": "auto", # auto, asgi3, asgi2, wsgi - "ws": "auto", # auto, websockets, wsproto - "log_level": logging.WARNING, - "backlog": 4096, # default=2048 - "timeout_keep_alive": 60, # default=5 - "ssl_keyfile": shared.cmd_opts.tls_keyfile, - "ssl_certfile": shared.cmd_opts.tls_certfile, - } - shared.log.info(f'API server: Uvicorn options={config}') - uvicorn.run(self.app, **config) - - def launch_hypercorn(self): - import asyncio - import hypercorn - import hypercorn.asyncio - config = hypercorn.config.Config() - config.bind = [f'{"0.0.0.0" if shared.cmd_opts.listen else "127.0.0.1"}:{shared.cmd_opts.port if shared.cmd_opts.port else 7861}'] - config.keyfile = shared.cmd_opts.tls_keyfile - config.certfile = shared.cmd_opts.tls_certfile - config.keep_alive_timeout = 60 # default=5 - config.backlog = 4096 # default=100 - config.loglevel = "WARNING" - config.max_app_queue_size = 64 # default=10 - shared.log.info(f'API server: Hypercorn options={vars(config)}') - instance = hypercorn.asyncio.serve(self.app, config) - asyncio.run(instance) - def launch(self): - self.launch_uvicorn() + config = { + "listen": shared.cmd_opts.listen, + "port": shared.cmd_opts.port, + "keyfile": shared.cmd_opts.tls_keyfile, + "certfile": shared.cmd_opts.tls_certfile, + "loop": "auto", + "http": "auto", + } + from modules.server import UvicornServer + server = UvicornServer(self.app, **config) + # from modules.server import HypercornServer + # server = HypercornServer(self.app, **config) + server.start() + shared.log.info(f'API server: Uvicorn options={config}') + return server diff --git a/modules/sd_models.py b/modules/sd_models.py index 79a0d9091..fc3a51fb8 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -332,7 +332,6 @@ def read_state_dict(checkpoint_file, map_location=None): # pylint: disable=unuse def get_checkpoint_state_dict(checkpoint_info: CheckpointInfo, timer): if checkpoint_info in checkpoints_loaded: - # use checkpoint cache shared.log.info("Model weights loading: from cache") return checkpoints_loaded[checkpoint_info] res = read_state_dict(checkpoint_info.filename) diff --git a/modules/server.py b/modules/server.py new file mode 100644 index 000000000..14645a02d --- /dev/null +++ b/modules/server.py @@ -0,0 +1,85 @@ +import threading +import logging +import uvicorn +import fastapi + + +class UvicornServer(uvicorn.Server): + def __init__(self, app: fastapi.FastAPI, listen = None, port = None, keyfile = None, certfile = None, loop = "auto", http = "auto"): + self.app: fastapi.FastAPI = app + self.thread: threading.Thread = None + self.wants_restart = False + self.config = uvicorn.Config( + app=self.app, + host = "0.0.0.0" if listen else "127.0.0.1", + port = port or 7861, + loop = loop, # auto, asyncio, uvloop + http = http, # auto, h11, httptools + interface = "auto", # auto, asgi3, asgi2, wsgi + ws = "auto", # auto, websockets, wsproto + log_level = logging.WARNING, + backlog = 4096, # default=2048 + timeout_keep_alive = 60, # default=5 + ssl_keyfile = keyfile, + ssl_certfile = certfile, + ws_max_size = 1024 * 1024 * 1024, # default 16MB + ) + super().__init__(config=self.config) + + def start(self): + self.thread = threading.Thread(target=self.run, daemon=True) + self.wants_restart = False + self.thread.start() + + def stop(self): + self.should_exit = True + self.thread.join() + + def restart(self): + self.wants_restart = True + self.stop() + self.start() + + +class HypercornServer(): + def __init__(self, app: fastapi.FastAPI, listen = None, port = None, keyfile = None, certfile = None, loop = "auto", http = None): + import asyncio + import hypercorn + self.app: fastapi.FastAPI = app + self.server: HypercornServer = None + self.thread = None + self.task = None + self.wants_restart = False + self.loop = 'trio' if loop == 'auto' else loop # asyncio, uvloop, trio + self.config = hypercorn.config.Config() + self.config.bind = [f'{"0.0.0.0" if listen else "127.0.0.1"}:{port or 7861}'] + self.config.keyfile = keyfile + self.config.certfile = certfile + self.config.keep_alive_timeout = 60 # default=5 + self.config.backlog = 4096 # default=100 + self.config.loglevel = "WARNING" + self.config.max_app_queue_size = 64 # default=10 + self.http = http # unused + self.main_loop = asyncio.get_event_loop() + + def run(self): + import trio + from hypercorn.trio import serve + self.server = trio.run(serve, self.app, self.config) + + def start(self): + if self.loop == 'trio': + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + elif self.loop == 'asyncio': # does not run in thread + import asyncio + from hypercorn.asyncio import serve + self.server = serve(self.app, self.config) + asyncio.run(self.server) + elif self.loop == 'uvloop': # does not run in thread + import uvloop + from hypercorn.asyncio import serve + uvloop.install() + from hypercorn.asyncio import serve + self.server = serve(self.app, self.config) + asyncio.run(self.server) diff --git a/modules/ui.py b/modules/ui.py index c7c106009..dc9a322e3 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -237,10 +237,10 @@ def create_toprow(is_img2img): pause.click(fn=lambda: modules.shared.state.pause(), _js='checkPaused', inputs=[], outputs=[]) with gr.Row(elem_id=f"{id_part}_tools"): paste = ToolButton(value=paste_symbol, elem_id="paste") - clear_prompt_button = ToolButton(value=clear_prompt_symbol, elem_id=f"{id_part}_clear_prompt") - extra_networks_button = ToolButton(value=extra_networks_symbol, elem_id=f"{id_part}_extra_networks") - prompt_style_apply = ToolButton(value=apply_style_symbol, elem_id=f"{id_part}_style_apply") - save_style = ToolButton(value=save_style_symbol, elem_id=f"{id_part}_style_create") + clear_prompt_button = ToolButton(value=clear_prompt_symbol, elem_id=f"{id_part}_clear_prompt_btn") + extra_networks_button = ToolButton(value=extra_networks_symbol, elem_id=f"{id_part}_extra_networks_btn") + prompt_style_apply = ToolButton(value=apply_style_symbol, elem_id=f"{id_part}_style_apply_btn") + save_style = ToolButton(value=save_style_symbol, elem_id=f"{id_part}_style_create_btn") clear_prompt_button.click(fn=lambda *x: x, _js="confirm_clear_prompt", inputs=[prompt, negative_prompt], outputs=[prompt, negative_prompt]) with gr.Row(elem_id=f"{id_part}_counters"): token_counter = gr.HTML(value="0/75", elem_id=f"{id_part}_token_counter", elem_classes=["token-counter"]) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index d59e7202a..063a7faa2 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -88,6 +88,10 @@ class ExtraNetworksPage: return abspath[len(parentdir):].replace('\\', '/') return "" + def is_empty(self, folder): + files = [f for f in os.listdir(folder) if f.lower().endswith(".ckpt") or f.lower().endswith(".safetensors") or f.lower().endswith(".pt")] + return len(files) == 0 + def create_html(self, tabname): view = shared.opts.extra_networks_default_view items_html = '' @@ -103,15 +107,13 @@ class ExtraNetworksPage: subdir = os.path.abspath(x)[len(parentdir):].replace("\\", "/") while subdir.startswith("/"): subdir = subdir[1:] - is_empty = len(os.listdir(x)) == 0 - if not is_empty and not subdir.endswith("/"): - subdir = subdir + "/" - subdirs[subdir] = 1 + if not self.is_empty(x): + subdirs[subdir] = 1 if subdirs: subdirs = {"": 1, **subdirs} subdirs_html = "".join([f""" """ for subdir in subdirs]) try: self.items = list(self.list_items()) diff --git a/webui.py b/webui.py index 093c0fdf4..9171e773a 100644 --- a/webui.py +++ b/webui.py @@ -258,6 +258,7 @@ def start_ui(): prevent_thread_lock=True, max_threads=64, show_api=True, + quiet=True, favicon_path='html/logo.ico', allowed_paths=[os.path.dirname(__file__), cmd_opts.data_dir], app_kwargs=fastapi_args, @@ -335,8 +336,8 @@ def api_only(): modules.script_callbacks.app_started_callback(None, app) modules.sd_models.write_metadata() log.info(f"Startup time: {startup_timer.summary()}") - api.launch() - return api + server = api.launch() + return server if __name__ == "__main__":