nicegui.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import asyncio
  2. import os
  3. import socket
  4. import time
  5. import urllib.parse
  6. from pathlib import Path
  7. from typing import Dict, Optional
  8. from fastapi import HTTPException, Request
  9. from fastapi.middleware.gzip import GZipMiddleware
  10. from fastapi.responses import FileResponse, Response
  11. from fastapi.staticfiles import StaticFiles
  12. from fastapi_socketio import SocketManager
  13. from nicegui import json
  14. from nicegui.json import NiceGUIJSONResponse
  15. from . import __version__, background_tasks, binding, favicon, globals, outbox
  16. from .app import App
  17. from .client import Client
  18. from .dependencies import js_components, libraries
  19. from .element import Element
  20. from .error import error_content
  21. from .helpers import is_file, safe_invoke
  22. from .page import page
  23. globals.app = app = App(default_response_class=NiceGUIJSONResponse)
  24. # NOTE we use custom json module which wraps orjson
  25. socket_manager = SocketManager(app=app, mount_location='/_nicegui_ws/', json=json)
  26. globals.sio = sio = socket_manager._sio
  27. app.add_middleware(GZipMiddleware)
  28. static_files = StaticFiles(
  29. directory=(Path(__file__).parent / 'static').resolve(),
  30. follow_symlink=True,
  31. )
  32. app.mount(f'/_nicegui/{__version__}/static', static_files, name='static')
  33. globals.index_client = Client(page('/'), shared=True).__enter__()
  34. @app.get('/')
  35. def index(request: Request) -> Response:
  36. return globals.index_client.build_response(request)
  37. @app.get(f'/_nicegui/{__version__}' + '/library/{name}/{file}')
  38. def get_dependencies(name: str, file: str):
  39. if name in libraries and libraries[name]['path'].exists():
  40. filepath = Path(libraries[name]['path']).parent / file
  41. if filepath.exists() and not filepath.is_dir():
  42. return FileResponse(filepath, media_type='text/javascript')
  43. return FileResponse(libraries[name]['path'], media_type='text/javascript')
  44. raise HTTPException(status_code=404, detail=f'dependency "{name}" not found')
  45. @app.get(f'/_nicegui/{__version__}' + '/components/{name}')
  46. def get_components(name: str):
  47. if name in js_components and js_components[name]['path'].exists():
  48. return FileResponse(js_components[name]['path'], media_type='text/javascript')
  49. raise HTTPException(status_code=404, detail=f'library "{name}" not found')
  50. @app.on_event('startup')
  51. def handle_startup(with_welcome_message: bool = True) -> None:
  52. if not globals.ui_run_has_been_called:
  53. raise RuntimeError('\n\n'
  54. 'You must call ui.run() to start the server.\n'
  55. 'If ui.run() is behind a main guard\n'
  56. ' if __name__ == "__main__":\n'
  57. 'remove the guard or replace it with\n'
  58. ' if __name__ in {"__main__", "__mp_main__"}:\n'
  59. 'to allow for multiprocessing.')
  60. if globals.favicon:
  61. if is_file(globals.favicon):
  62. globals.app.add_route('/favicon.ico', lambda _: FileResponse(globals.favicon))
  63. else:
  64. globals.app.add_route('/favicon.ico', lambda _: favicon.get_favicon_response())
  65. else:
  66. globals.app.add_route('/favicon.ico', lambda _: FileResponse(Path(__file__).parent / 'static' / 'favicon.ico'))
  67. globals.state = globals.State.STARTING
  68. globals.loop = asyncio.get_running_loop()
  69. with globals.index_client:
  70. for t in globals.startup_handlers:
  71. safe_invoke(t)
  72. background_tasks.create(binding.loop())
  73. background_tasks.create(outbox.loop())
  74. background_tasks.create(prune_clients())
  75. background_tasks.create(prune_slot_stacks())
  76. globals.state = globals.State.STARTED
  77. if with_welcome_message:
  78. print_welcome_message()
  79. def print_welcome_message():
  80. host = os.environ['NICEGUI_HOST']
  81. port = os.environ['NICEGUI_PORT']
  82. ips = set()
  83. if host == '0.0.0.0':
  84. try:
  85. ips.update(set(info[4][0] for info in socket.getaddrinfo(socket.gethostname(), None) if len(info[4]) == 2))
  86. except Exception:
  87. pass # NOTE: if we can't get the host's IP, we'll just use localhost
  88. ips.discard('127.0.0.1')
  89. addresses = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
  90. if len(addresses) >= 2:
  91. addresses[-1] = 'and ' + addresses[-1]
  92. print(f'NiceGUI ready to go on {", ".join(addresses)}', flush=True)
  93. @app.on_event('shutdown')
  94. async def handle_shutdown() -> None:
  95. if app.native.main_window:
  96. app.native.main_window.signal_server_shutdown()
  97. globals.state = globals.State.STOPPING
  98. with globals.index_client:
  99. for t in globals.shutdown_handlers:
  100. safe_invoke(t)
  101. globals.state = globals.State.STOPPED
  102. @app.exception_handler(404)
  103. async def exception_handler_404(request: Request, exception: Exception) -> Response:
  104. globals.log.warning(f'{request.url} not found')
  105. with Client(page('')) as client:
  106. error_content(404, exception)
  107. return client.build_response(request, 404)
  108. @app.exception_handler(Exception)
  109. async def exception_handler_500(request: Request, exception: Exception) -> Response:
  110. globals.log.exception(exception)
  111. with Client(page('')) as client:
  112. error_content(500, exception)
  113. return client.build_response(request, 500)
  114. @sio.on('handshake')
  115. def handle_handshake(sid: str) -> bool:
  116. client = get_client(sid)
  117. if not client:
  118. return False
  119. client.environ = sio.get_environ(sid)
  120. sio.enter_room(sid, client.id)
  121. for t in client.connect_handlers:
  122. safe_invoke(t, client)
  123. for t in globals.connect_handlers:
  124. safe_invoke(t, client)
  125. return True
  126. @sio.on('disconnect')
  127. def handle_disconnect(sid: str) -> None:
  128. client = get_client(sid)
  129. if not client:
  130. return
  131. if not client.shared:
  132. delete_client(client.id)
  133. for t in client.disconnect_handlers:
  134. safe_invoke(t, client)
  135. for t in globals.disconnect_handlers:
  136. safe_invoke(t, client)
  137. @sio.on('event')
  138. def handle_event(sid: str, msg: Dict) -> None:
  139. client = get_client(sid)
  140. if not client or not client.has_socket_connection:
  141. return
  142. with client:
  143. sender = client.elements.get(msg['id'])
  144. if sender:
  145. msg['args'] = [json.loads(arg) for arg in msg.get('args', [])]
  146. if len(msg['args']) == 1:
  147. msg['args'] = msg['args'][0]
  148. sender._handle_event(msg)
  149. @sio.on('javascript_response')
  150. def handle_javascript_response(sid: str, msg: Dict) -> None:
  151. client = get_client(sid)
  152. if not client:
  153. return
  154. client.waiting_javascript_commands[msg['request_id']] = msg['result']
  155. def get_client(sid: str) -> Optional[Client]:
  156. query_bytes: bytearray = sio.get_environ(sid)['asgi.scope']['query_string']
  157. query = urllib.parse.parse_qs(query_bytes.decode())
  158. client_id = query['client_id'][0]
  159. return globals.clients.get(client_id)
  160. async def prune_clients() -> None:
  161. while True:
  162. stale = [
  163. id
  164. for id, client in globals.clients.items()
  165. if not client.shared and not client.has_socket_connection and client.created < time.time() - 60.0
  166. ]
  167. for id in stale:
  168. delete_client(id)
  169. await asyncio.sleep(10)
  170. async def prune_slot_stacks() -> None:
  171. while True:
  172. running = [
  173. id(task)
  174. for task in asyncio.tasks.all_tasks()
  175. if not task.done() and not task.cancelled()
  176. ]
  177. stale = [
  178. id_
  179. for id_ in globals.slot_stacks
  180. if id_ not in running
  181. ]
  182. for id_ in stale:
  183. del globals.slot_stacks[id_]
  184. await asyncio.sleep(10)
  185. def delete_client(id: str) -> None:
  186. binding.remove(list(globals.clients[id].elements.values()), Element)
  187. for element in globals.clients[id].elements.values():
  188. element.delete()
  189. del globals.clients[id]