|
@@ -1,169 +1,44 @@
|
|
|
-import multiprocessing
|
|
|
-import os
|
|
|
+import asyncio
|
|
|
import sys
|
|
|
-from pathlib import Path
|
|
|
-from typing import Any, List, Literal, Optional, Tuple, Union
|
|
|
+from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
|
+from functools import partial
|
|
|
+from typing import Any, Callable
|
|
|
|
|
|
-import __main__
|
|
|
-from starlette.routing import Route
|
|
|
-from uvicorn.main import STARTUP_FAILURE
|
|
|
-from uvicorn.supervisors import ChangeReload, Multiprocess
|
|
|
+from . import globals, helpers # pylint: disable=redefined-builtin
|
|
|
|
|
|
-from . import air, globals, helpers # pylint: disable=redefined-builtin
|
|
|
-from . import native as native_module
|
|
|
-from .client import Client
|
|
|
-from .language import Language
|
|
|
-from .logging import log
|
|
|
-from .server import CustomServerConfig, Server
|
|
|
+process_pool = ProcessPoolExecutor()
|
|
|
+thread_pool = ThreadPoolExecutor()
|
|
|
|
|
|
-APP_IMPORT_STRING = 'nicegui:app'
|
|
|
|
|
|
-
|
|
|
-def run(*,
|
|
|
- host: Optional[str] = None,
|
|
|
- port: int = 8080,
|
|
|
- title: str = 'NiceGUI',
|
|
|
- viewport: str = 'width=device-width, initial-scale=1',
|
|
|
- favicon: Optional[Union[str, Path]] = None,
|
|
|
- dark: Optional[bool] = False,
|
|
|
- language: Language = 'en-US',
|
|
|
- binding_refresh_interval: float = 0.1,
|
|
|
- reconnect_timeout: float = 3.0,
|
|
|
- show: bool = True,
|
|
|
- on_air: Optional[Union[str, Literal[True]]] = None,
|
|
|
- native: bool = False,
|
|
|
- window_size: Optional[Tuple[int, int]] = None,
|
|
|
- fullscreen: bool = False,
|
|
|
- frameless: bool = False,
|
|
|
- reload: bool = True,
|
|
|
- uvicorn_logging_level: str = 'warning',
|
|
|
- uvicorn_reload_dirs: str = '.',
|
|
|
- uvicorn_reload_includes: str = '*.py',
|
|
|
- uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
|
|
|
- tailwind: bool = True,
|
|
|
- prod_js: bool = True,
|
|
|
- endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
|
|
|
- storage_secret: Optional[str] = None,
|
|
|
- **kwargs: Any,
|
|
|
- ) -> None:
|
|
|
- '''ui.run
|
|
|
-
|
|
|
- You can call `ui.run()` with optional arguments:
|
|
|
-
|
|
|
- :param host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
|
|
|
- :param port: use this port (default: `8080`)
|
|
|
- :param title: page title (default: `'NiceGUI'`, can be overwritten per page)
|
|
|
- :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
|
|
|
- :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
|
|
|
- :param dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
|
|
|
- :param language: language for Quasar elements (default: `'en-US'`)
|
|
|
- :param binding_refresh_interval: time between binding updates (default: `0.1` seconds, bigger is more CPU friendly)
|
|
|
- :param reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 3.0 seconds)
|
|
|
- :param show: automatically open the UI in a browser tab (default: `True`)
|
|
|
- :param on_air: tech preview: `allows temporary remote access <https://nicegui.io/documentation#nicegui_on_air>`_ if set to `True` (default: disabled)
|
|
|
- :param native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
|
|
|
- :param window_size: open the UI in a native window with the provided size (e.g. `(1024, 786)`, default: `None`, also activates `native`)
|
|
|
- :param fullscreen: open the UI in a fullscreen window (default: `False`, also activates `native`)
|
|
|
- :param frameless: open the UI in a frameless window (default: `False`, also activates `native`)
|
|
|
- :param reload: automatically reload the UI on file changes (default: `True`)
|
|
|
- :param uvicorn_logging_level: logging level for uvicorn server (default: `'warning'`)
|
|
|
- :param uvicorn_reload_dirs: string with comma-separated list for directories to be monitored (default is current working directory only)
|
|
|
- :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'.py'`)
|
|
|
- :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
|
|
|
- :param tailwind: whether to use Tailwind (experimental, default: `True`)
|
|
|
- :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
|
|
|
- :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
|
|
|
- :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
|
|
|
- :param kwargs: additional keyword arguments are passed to `uvicorn.run`
|
|
|
- '''
|
|
|
- globals.ui_run_has_been_called = True
|
|
|
- globals.reload = reload
|
|
|
- globals.title = title
|
|
|
- globals.viewport = viewport
|
|
|
- globals.favicon = favicon
|
|
|
- globals.dark = dark
|
|
|
- globals.language = language
|
|
|
- globals.binding_refresh_interval = binding_refresh_interval
|
|
|
- globals.reconnect_timeout = reconnect_timeout
|
|
|
- globals.tailwind = tailwind
|
|
|
- globals.prod_js = prod_js
|
|
|
- globals.endpoint_documentation = endpoint_documentation
|
|
|
-
|
|
|
- for route in globals.app.routes:
|
|
|
- if not isinstance(route, Route):
|
|
|
- continue
|
|
|
- if route.path.startswith('/_nicegui') and hasattr(route, 'methods'):
|
|
|
- route.include_in_schema = endpoint_documentation in {'internal', 'all'}
|
|
|
- if route.path == '/' or route.path in Client.page_routes.values():
|
|
|
- route.include_in_schema = endpoint_documentation in {'page', 'all'}
|
|
|
-
|
|
|
- if on_air:
|
|
|
- air.instance = air.Air('' if on_air is True else on_air)
|
|
|
-
|
|
|
- if multiprocessing.current_process().name != 'MainProcess':
|
|
|
+async def _run(executor: Any, callback: Callable, *args: Any, **kwargs: Any) -> Any:
|
|
|
+ if globals.app.is_stopping:
|
|
|
return
|
|
|
+ try:
|
|
|
+ loop = asyncio.get_running_loop()
|
|
|
+ return await loop.run_in_executor(executor, partial(callback, *args, **kwargs))
|
|
|
+ except RuntimeError as e:
|
|
|
+ if 'cannot schedule new futures after shutdown' not in str(e):
|
|
|
+ raise
|
|
|
+ except asyncio.exceptions.CancelledError:
|
|
|
+ pass
|
|
|
|
|
|
- if reload and not hasattr(__main__, '__file__'):
|
|
|
- log.warning('auto-reloading is only supported when running from a file')
|
|
|
- globals.reload = reload = False
|
|
|
|
|
|
- if fullscreen:
|
|
|
- native = True
|
|
|
- if frameless:
|
|
|
- native = True
|
|
|
- if window_size:
|
|
|
- native = True
|
|
|
- if native:
|
|
|
- show = False
|
|
|
- host = host or '127.0.0.1'
|
|
|
- port = native_module.find_open_port()
|
|
|
- width, height = window_size or (800, 600)
|
|
|
- native_module.activate(host, port, title, width, height, fullscreen, frameless)
|
|
|
- else:
|
|
|
- host = host or '0.0.0.0'
|
|
|
+async def cpu_bound(callback: Callable, *args: Any, **kwargs: Any) -> Any:
|
|
|
+ """Run a CPU-bound function in a separate process."""
|
|
|
+ return await _run(process_pool, callback, *args, **kwargs)
|
|
|
|
|
|
- # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
|
|
|
- os.environ['NICEGUI_HOST'] = host
|
|
|
- os.environ['NICEGUI_PORT'] = str(port)
|
|
|
|
|
|
- if show:
|
|
|
- helpers.schedule_browser(host, port)
|
|
|
+async def io_bound(callback: Callable, *args: Any, **kwargs: Any) -> Any:
|
|
|
+ """Run an I/O-bound function in a separate thread."""
|
|
|
+ return await _run(thread_pool, callback, *args, **kwargs)
|
|
|
|
|
|
- def split_args(args: str) -> List[str]:
|
|
|
- return [a.strip() for a in args.split(',')]
|
|
|
|
|
|
- # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
|
|
|
-
|
|
|
- config = CustomServerConfig(
|
|
|
- APP_IMPORT_STRING if reload else globals.app,
|
|
|
- host=host,
|
|
|
- port=port,
|
|
|
- reload=reload,
|
|
|
- reload_includes=split_args(uvicorn_reload_includes) if reload else None,
|
|
|
- reload_excludes=split_args(uvicorn_reload_excludes) if reload else None,
|
|
|
- reload_dirs=split_args(uvicorn_reload_dirs) if reload else None,
|
|
|
- log_level=uvicorn_logging_level,
|
|
|
- **kwargs,
|
|
|
- )
|
|
|
- config.storage_secret = storage_secret
|
|
|
- config.method_queue = native_module.method_queue if native else None
|
|
|
- config.response_queue = native_module.response_queue if native else None
|
|
|
- Server.create_singleton(config)
|
|
|
-
|
|
|
- if (reload or config.workers > 1) and not isinstance(config.app, str):
|
|
|
- log.warning('You must pass the application as an import string to enable "reload" or "workers".')
|
|
|
- sys.exit(1)
|
|
|
-
|
|
|
- if config.should_reload:
|
|
|
- sock = config.bind_socket()
|
|
|
- ChangeReload(config, target=Server.instance.run, sockets=[sock]).run()
|
|
|
- elif config.workers > 1:
|
|
|
- sock = config.bind_socket()
|
|
|
- Multiprocess(config, target=Server.instance.run, sockets=[sock]).run()
|
|
|
- else:
|
|
|
- Server.instance.run()
|
|
|
- if config.uds:
|
|
|
- os.remove(config.uds) # pragma: py-win32
|
|
|
-
|
|
|
- if not Server.instance.started and not config.should_reload and config.workers == 1:
|
|
|
- sys.exit(STARTUP_FAILURE)
|
|
|
+def tear_down() -> None:
|
|
|
+ """Kill all processes and threads."""
|
|
|
+ if helpers.is_pytest():
|
|
|
+ return
|
|
|
+ for p in process_pool._processes.values(): # pylint: disable=protected-access
|
|
|
+ p.kill()
|
|
|
+ kwargs = {'cancel_futures': True} if sys.version_info >= (3, 9) else {}
|
|
|
+ process_pool.shutdown(wait=True, **kwargs)
|
|
|
+ thread_pool.shutdown(wait=False, **kwargs)
|