run.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import multiprocessing
  2. import os
  3. import socket
  4. import sys
  5. from pathlib import Path
  6. from typing import Any, List, Literal, Optional, Tuple, Union
  7. import __main__
  8. import uvicorn
  9. from starlette.routing import Route
  10. from uvicorn.main import STARTUP_FAILURE
  11. from uvicorn.supervisors import ChangeReload, Multiprocess
  12. from . import native_mode # pylint: disable=redefined-builtin
  13. from . import storage # pylint: disable=redefined-builtin
  14. from . import air, globals, helpers # pylint: disable=redefined-builtin
  15. from . import native as native_module
  16. from .client import Client
  17. from .language import Language
  18. from .logging import log
  19. APP_IMPORT_STRING = 'nicegui:app'
  20. class CustomServerConfig(uvicorn.Config):
  21. storage_secret: Optional[str] = None
  22. method_queue: Optional[multiprocessing.Queue] = None
  23. response_queue: Optional[multiprocessing.Queue] = None
  24. class Server(uvicorn.Server):
  25. def run(self, sockets: Optional[List[socket.socket]] = None) -> None:
  26. globals.server = self
  27. assert isinstance(self.config, CustomServerConfig)
  28. if self.config.method_queue is not None and self.config.response_queue is not None:
  29. native_module.method_queue = self.config.method_queue
  30. native_module.response_queue = self.config.response_queue
  31. globals.app.native.main_window = native_module.WindowProxy()
  32. storage.set_storage_secret(self.config.storage_secret)
  33. super().run(sockets=sockets)
  34. def run(*,
  35. host: Optional[str] = None,
  36. port: int = 8080,
  37. title: str = 'NiceGUI',
  38. viewport: str = 'width=device-width, initial-scale=1',
  39. favicon: Optional[Union[str, Path]] = None,
  40. dark: Optional[bool] = False,
  41. language: Language = 'en-US',
  42. binding_refresh_interval: float = 0.1,
  43. reconnect_timeout: float = 3.0,
  44. show: bool = True,
  45. on_air: Optional[Union[str, Literal[True]]] = None,
  46. native: bool = False,
  47. window_size: Optional[Tuple[int, int]] = None,
  48. fullscreen: bool = False,
  49. frameless: bool = False,
  50. reload: bool = True,
  51. uvicorn_logging_level: str = 'warning',
  52. uvicorn_reload_dirs: str = '.',
  53. uvicorn_reload_includes: str = '*.py',
  54. uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
  55. tailwind: bool = True,
  56. prod_js: bool = True,
  57. endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
  58. storage_secret: Optional[str] = None,
  59. **kwargs: Any,
  60. ) -> None:
  61. '''ui.run
  62. You can call `ui.run()` with optional arguments:
  63. :param host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
  64. :param port: use this port (default: `8080`)
  65. :param title: page title (default: `'NiceGUI'`, can be overwritten per page)
  66. :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
  67. :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
  68. :param dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
  69. :param language: language for Quasar elements (default: `'en-US'`)
  70. :param binding_refresh_interval: time between binding updates (default: `0.1` seconds, bigger is more CPU friendly)
  71. :param reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 3.0 seconds)
  72. :param show: automatically open the UI in a browser tab (default: `True`)
  73. :param on_air: tech preview: `allows temporary remote access <https://nicegui.io/documentation#nicegui_on_air>`_ if set to `True` (default: disabled)
  74. :param native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
  75. :param window_size: open the UI in a native window with the provided size (e.g. `(1024, 786)`, default: `None`, also activates `native`)
  76. :param fullscreen: open the UI in a fullscreen window (default: `False`, also activates `native`)
  77. :param frameless: open the UI in a frameless window (default: `False`, also activates `native`)
  78. :param reload: automatically reload the UI on file changes (default: `True`)
  79. :param uvicorn_logging_level: logging level for uvicorn server (default: `'warning'`)
  80. :param uvicorn_reload_dirs: string with comma-separated list for directories to be monitored (default is current working directory only)
  81. :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'.py'`)
  82. :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
  83. :param tailwind: whether to use Tailwind (experimental, default: `True`)
  84. :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
  85. :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
  86. :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
  87. :param kwargs: additional keyword arguments are passed to `uvicorn.run`
  88. '''
  89. globals.ui_run_has_been_called = True
  90. globals.reload = reload
  91. globals.title = title
  92. globals.viewport = viewport
  93. globals.favicon = favicon
  94. globals.dark = dark
  95. globals.language = language
  96. globals.binding_refresh_interval = binding_refresh_interval
  97. globals.reconnect_timeout = reconnect_timeout
  98. globals.tailwind = tailwind
  99. globals.prod_js = prod_js
  100. globals.endpoint_documentation = endpoint_documentation
  101. for route in globals.app.routes:
  102. if not isinstance(route, Route):
  103. continue
  104. if route.path.startswith('/_nicegui') and hasattr(route, 'methods'):
  105. route.include_in_schema = endpoint_documentation in {'internal', 'all'}
  106. if route.path == '/' or route.path in Client.page_routes.values():
  107. route.include_in_schema = endpoint_documentation in {'page', 'all'}
  108. if on_air:
  109. air.instance = air.Air('' if on_air is True else on_air)
  110. if multiprocessing.current_process().name != 'MainProcess':
  111. return
  112. if reload and not hasattr(__main__, '__file__'):
  113. log.warning('auto-reloading is only supported when running from a file')
  114. globals.reload = reload = False
  115. if fullscreen:
  116. native = True
  117. if frameless:
  118. native = True
  119. if window_size:
  120. native = True
  121. if native:
  122. show = False
  123. host = host or '127.0.0.1'
  124. port = native_mode.find_open_port()
  125. width, height = window_size or (800, 600)
  126. native_mode.activate(host, port, title, width, height, fullscreen, frameless)
  127. else:
  128. host = host or '0.0.0.0'
  129. # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
  130. os.environ['NICEGUI_HOST'] = host
  131. os.environ['NICEGUI_PORT'] = str(port)
  132. if show:
  133. helpers.schedule_browser(host, port)
  134. def split_args(args: str) -> List[str]:
  135. return [a.strip() for a in args.split(',')]
  136. # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
  137. config = CustomServerConfig(
  138. APP_IMPORT_STRING if reload else globals.app,
  139. host=host,
  140. port=port,
  141. reload=reload,
  142. reload_includes=split_args(uvicorn_reload_includes) if reload else None,
  143. reload_excludes=split_args(uvicorn_reload_excludes) if reload else None,
  144. reload_dirs=split_args(uvicorn_reload_dirs) if reload else None,
  145. log_level=uvicorn_logging_level,
  146. **kwargs,
  147. )
  148. config.storage_secret = storage_secret
  149. config.method_queue = native_module.method_queue if native else None
  150. config.response_queue = native_module.response_queue if native else None
  151. globals.server = Server(config=config)
  152. if (reload or config.workers > 1) and not isinstance(config.app, str):
  153. log.warning('You must pass the application as an import string to enable "reload" or "workers".')
  154. sys.exit(1)
  155. if config.should_reload:
  156. sock = config.bind_socket()
  157. ChangeReload(config, target=globals.server.run, sockets=[sock]).run()
  158. elif config.workers > 1:
  159. sock = config.bind_socket()
  160. Multiprocess(config, target=globals.server.run, sockets=[sock]).run()
  161. else:
  162. globals.server.run()
  163. if config.uds:
  164. os.remove(config.uds) # pragma: py-win32
  165. if not globals.server.started and not config.should_reload and config.workers == 1:
  166. sys.exit(STARTUP_FAILURE)