native_mode.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import _thread
  2. import logging
  3. import multiprocessing as mp
  4. import queue
  5. import socket
  6. import sys
  7. import tempfile
  8. import time
  9. import warnings
  10. from threading import Event, Thread
  11. from typing import Any, Callable, Dict, List, Tuple
  12. from . import globals, helpers, native
  13. try:
  14. with warnings.catch_warnings():
  15. # webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
  16. warnings.filterwarnings('ignore', category=DeprecationWarning)
  17. import webview
  18. except ModuleNotFoundError:
  19. class webview:
  20. class Window:
  21. pass
  22. pass
  23. def open_window(
  24. host: str, port: int, title: str, width: int, height: int, fullscreen: bool,
  25. method_queue: mp.Queue, response_queue: mp.Queue,
  26. ) -> None:
  27. while not helpers.is_port_open(host, port):
  28. time.sleep(0.1)
  29. window_kwargs = dict(url=f'http://{host}:{port}', title=title, width=width, height=height, fullscreen=fullscreen)
  30. window_kwargs.update(globals.app.native.window_args)
  31. try:
  32. window = webview.create_window(**window_kwargs)
  33. closing = Event()
  34. window.events.closing += closing.set
  35. start_window_method_executor(window, method_queue, response_queue, closing)
  36. webview.start(storage_path=tempfile.mkdtemp(), **globals.app.native.start_args)
  37. except NameError:
  38. logging.error('''Native mode is not supported in this configuration.
  39. Please run "pip install nicegui[native] to use it.''')
  40. sys.exit(1)
  41. def start_window_method_executor(
  42. window: webview.Window, method_queue: mp.Queue, response_queue: mp.Queue, closing: Event
  43. ) -> None:
  44. def execute(method: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> None:
  45. try:
  46. response = method(*args, **kwargs)
  47. if response is not None or 'dialog' in method.__name__:
  48. response_queue.put(response)
  49. except Exception:
  50. logging.exception(f'error in window.{method.__name__}')
  51. def window_method_executor() -> None:
  52. pending_executions: List[Thread] = []
  53. while not closing.is_set():
  54. try:
  55. method_name, args, kwargs = method_queue.get(block=False)
  56. if method_name == 'signal_server_shutdown':
  57. if pending_executions:
  58. logging.warning('shutdown is possibly blocked by opened dialogs like a file picker')
  59. while pending_executions:
  60. pending_executions.pop().join()
  61. elif method_name == 'get_always_on_top':
  62. response_queue.put(window.on_top)
  63. elif method_name == 'set_always_on_top':
  64. window.on_top = args[0]
  65. elif method_name == 'get_position':
  66. response_queue.put((int(window.x), int(window.y)))
  67. elif method_name == 'get_size':
  68. response_queue.put((int(window.width), int(window.height)))
  69. else:
  70. method = getattr(window, method_name)
  71. if callable(method):
  72. pending_executions.append(Thread(target=execute, args=(method, args, kwargs)))
  73. pending_executions[-1].start()
  74. else:
  75. logging.error(f'window.{method_name} is not callable')
  76. except queue.Empty:
  77. time.sleep(0.01)
  78. except Exception:
  79. logging.exception(f'error in window.{method_name}')
  80. Thread(target=window_method_executor).start()
  81. def activate(host: str, port: int, title: str, width: int, height: int, fullscreen: bool) -> None:
  82. def check_shutdown() -> None:
  83. while process.is_alive():
  84. time.sleep(0.1)
  85. globals.server.should_exit = True
  86. while globals.state != globals.State.STOPPED:
  87. time.sleep(0.1)
  88. _thread.interrupt_main()
  89. mp.freeze_support()
  90. args = host, port, title, width, height, fullscreen, native.method_queue, native.response_queue
  91. process = mp.Process(target=open_window, args=args, daemon=False)
  92. process.start()
  93. Thread(target=check_shutdown, daemon=True).start()
  94. def find_open_port(start_port: int = 8000, end_port: int = 8999) -> int:
  95. """Reliably find an open port in a given range.
  96. This function will actually try to open the port to ensure no firewall blocks it.
  97. This is better than, e.g., passing port=0 to uvicorn.
  98. """
  99. for port in range(start_port, end_port + 1):
  100. try:
  101. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  102. s.bind(('localhost', port))
  103. return port
  104. except OSError:
  105. pass
  106. raise OSError('No open port found')