137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import _thread
|
|
import multiprocessing as mp
|
|
import queue
|
|
import socket
|
|
import sys
|
|
import time
|
|
import warnings
|
|
from threading import Event, Thread
|
|
from typing import Any, Callable
|
|
|
|
from .. import core, helpers, optional_features
|
|
from ..logging import log
|
|
from ..server import Server
|
|
from . import native
|
|
|
|
try:
|
|
with warnings.catch_warnings():
|
|
# webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
|
|
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
|
import webview
|
|
optional_features.register('webview')
|
|
except ModuleNotFoundError:
|
|
pass
|
|
|
|
|
|
def _open_window(
|
|
host: str, port: int, title: str, width: int, height: int, fullscreen: bool, frameless: bool,
|
|
method_queue: mp.Queue, response_queue: mp.Queue,
|
|
) -> None:
|
|
while not helpers.is_port_open(host, port):
|
|
time.sleep(0.1)
|
|
|
|
window_kwargs = {
|
|
'url': f'http://{host}:{port}',
|
|
'title': title,
|
|
'width': width,
|
|
'height': height,
|
|
'fullscreen': fullscreen,
|
|
'frameless': frameless,
|
|
**core.app.native.window_args,
|
|
}
|
|
webview.settings.update(**core.app.native.settings)
|
|
window = webview.create_window(**window_kwargs)
|
|
assert window is not None
|
|
closed = Event()
|
|
window.events.closed += closed.set
|
|
_start_window_method_executor(window, method_queue, response_queue, closed)
|
|
webview.start(**core.app.native.start_args)
|
|
|
|
|
|
def _start_window_method_executor(window: webview.Window,
|
|
method_queue: mp.Queue,
|
|
response_queue: mp.Queue,
|
|
closed: Event) -> None:
|
|
def execute(method: Callable, args: tuple[Any, ...], kwargs: dict[str, Any]) -> None:
|
|
try:
|
|
response = method(*args, **kwargs)
|
|
if response is not None or 'dialog' in method.__name__:
|
|
response_queue.put(response)
|
|
except Exception:
|
|
log.exception(f'error in window.{method.__name__}')
|
|
|
|
def window_method_executor() -> None:
|
|
pending_executions: list[Thread] = []
|
|
while not closed.is_set():
|
|
try:
|
|
method_name, args, kwargs = method_queue.get(block=False)
|
|
if method_name == 'signal_server_shutdown':
|
|
if pending_executions:
|
|
log.warning('shutdown is possibly blocked by opened dialogs like a file picker')
|
|
while pending_executions:
|
|
pending_executions.pop().join()
|
|
elif method_name == 'get_always_on_top':
|
|
response_queue.put(window.on_top)
|
|
elif method_name == 'set_always_on_top':
|
|
window.on_top = args[0]
|
|
elif method_name == 'get_position':
|
|
response_queue.put((int(window.x), int(window.y)))
|
|
elif method_name == 'get_size':
|
|
response_queue.put((int(window.width), int(window.height)))
|
|
else:
|
|
method = getattr(window, method_name)
|
|
if callable(method):
|
|
pending_executions.append(Thread(target=execute, args=(method, args, kwargs)))
|
|
pending_executions[-1].start()
|
|
else:
|
|
log.error(f'window.{method_name} is not callable')
|
|
except queue.Empty:
|
|
time.sleep(0.016) # NOTE: avoid issue https://github.com/zauberzeug/nicegui/issues/2482 on Windows
|
|
except Exception:
|
|
log.exception(f'error in window.{method_name}')
|
|
|
|
Thread(target=window_method_executor).start()
|
|
|
|
|
|
def activate(host: str, port: int, title: str, width: int, height: int, fullscreen: bool, frameless: bool) -> None:
|
|
"""Activate native mode."""
|
|
def check_shutdown() -> None:
|
|
while process.is_alive():
|
|
time.sleep(0.1)
|
|
Server.instance.should_exit = True
|
|
while not core.app.is_stopped:
|
|
time.sleep(0.1)
|
|
_thread.interrupt_main()
|
|
native.remove_queues()
|
|
|
|
if not optional_features.has('webview'):
|
|
log.error('Native mode is not supported in this configuration.\n'
|
|
'Please run "pip install pywebview" to use it.')
|
|
sys.exit(1)
|
|
|
|
mp.freeze_support()
|
|
native.create_queues()
|
|
args = host, port, title, width, height, fullscreen, frameless, native.method_queue, native.response_queue
|
|
process = mp.Process(target=_open_window, args=args, daemon=True)
|
|
process.start()
|
|
|
|
Thread(target=check_shutdown, daemon=True).start()
|
|
|
|
|
|
def find_open_port(start_port: int = 8000, end_port: int = 8999) -> int:
|
|
"""Reliably find an open port in a given range.
|
|
|
|
This function will actually try to open the port to ensure no firewall blocks it.
|
|
This is better than, e.g., passing port=0 to uvicorn.
|
|
"""
|
|
for port in range(start_port, end_port + 1):
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(('localhost', port))
|
|
return port
|
|
except OSError:
|
|
pass
|
|
raise OSError('No open port found')
|