native_mode.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  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 . import globals, helpers, native
  12. try:
  13. with warnings.catch_warnings():
  14. # webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
  15. warnings.filterwarnings('ignore', category=DeprecationWarning)
  16. import webview
  17. except ModuleNotFoundError:
  18. pass
  19. def open_window(
  20. host: str, port: int, title: str, width: int, height: int, fullscreen: bool,
  21. method_queue: mp.Queue, response_queue: mp.Queue,
  22. ) -> None:
  23. while not helpers.is_port_open(host, port):
  24. time.sleep(0.1)
  25. window_kwargs = dict(url=f'http://{host}:{port}', title=title, width=width, height=height, fullscreen=fullscreen)
  26. window_kwargs.update(globals.app.native.window_args)
  27. try:
  28. window = webview.create_window(**window_kwargs)
  29. closing = Event()
  30. window.events.closing += lambda: closing.set()
  31. start_window_method_executor(window, method_queue, response_queue, closing)
  32. webview.start(storage_path=tempfile.mkdtemp(), **globals.app.native.start_args)
  33. except NameError:
  34. print('Native mode is not supported in this configuration. Please install pywebview to use it.')
  35. sys.exit(1)
  36. def start_window_method_executor(
  37. window: webview.Window, method_queue: mp.Queue, response_queue: mp.Queue, closing: Event
  38. ) -> None:
  39. def window_method_executor():
  40. while not closing.is_set():
  41. try:
  42. method, args, kwargs = method_queue.get(block=False)
  43. if method == 'get_position':
  44. response_queue.put((int(window.x), int(window.y)))
  45. continue
  46. if method == 'get_size':
  47. response_queue.put((int(window.width), int(window.height)))
  48. continue
  49. attr = getattr(window, method)
  50. if callable(attr):
  51. response = attr(*args, **kwargs)
  52. if response is not None:
  53. response_queue.put(response)
  54. else:
  55. logging.error(f'window.{method} is not callable')
  56. except queue.Empty:
  57. time.sleep(0.01)
  58. except:
  59. logging.exception(f'error in window.{method}')
  60. t = Thread(target=window_method_executor)
  61. t.start()
  62. def activate(host: str, port: int, title: str, width: int, height: int, fullscreen: bool) -> None:
  63. def check_shutdown() -> None:
  64. while process.is_alive():
  65. time.sleep(0.1)
  66. globals.server.should_exit = True
  67. while globals.state != globals.State.STOPPED:
  68. time.sleep(0.1)
  69. _thread.interrupt_main()
  70. mp.freeze_support()
  71. process = mp.Process(
  72. target=open_window,
  73. args=(host, port, title, width, height, fullscreen, native.method_queue, native.response_queue),
  74. daemon=False
  75. )
  76. process.start()
  77. Thread(target=check_shutdown, daemon=True).start()
  78. def find_open_port(start_port: int = 8000, end_port: int = 8999) -> int:
  79. """Reliably find an open port in a given range.
  80. This function will actually try to open the port to ensure no firewall blocks it.
  81. This is better than, e.g., passing port=0 to uvicorn.
  82. """
  83. for port in range(start_port, end_port + 1):
  84. try:
  85. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  86. s.bind(('localhost', port))
  87. return port
  88. except OSError:
  89. pass
  90. raise OSError('No open port found')