You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
128 lines
4.9 KiB
128 lines
4.9 KiB
from __future__ import annotations |
|
|
|
import _thread |
|
import multiprocessing as mp |
|
import queue |
|
import socket |
|
import sys |
|
import tempfile |
|
import time |
|
import warnings |
|
from threading import Event, Thread |
|
from typing import Any, Callable, Dict, List, Tuple |
|
|
|
from . import globals, helpers, native # pylint: disable=redefined-builtin |
|
|
|
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 |
|
globals.optional_features.add('native') |
|
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, |
|
**globals.app.native.window_args, |
|
} |
|
window = webview.create_window(**window_kwargs) |
|
closed = Event() |
|
window.events.closed += closed.set |
|
start_window_method_executor(window, method_queue, response_queue, closed) |
|
webview.start(storage_path=tempfile.mkdtemp(), **globals.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: |
|
globals.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: |
|
globals.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: |
|
globals.log.error(f'window.{method_name} is not callable') |
|
except queue.Empty: |
|
time.sleep(0.01) |
|
except Exception: |
|
globals.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: |
|
def check_shutdown() -> None: |
|
while process.is_alive(): |
|
time.sleep(0.1) |
|
globals.server.should_exit = True |
|
while globals.state != globals.State.STOPPED: |
|
time.sleep(0.1) |
|
_thread.interrupt_main() |
|
|
|
if 'native' not in globals.optional_features: |
|
globals.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() |
|
args = host, port, title, width, height, fullscreen, frameless, native.method_queue, native.response_queue |
|
process = mp.Process(target=open_window, args=args, daemon=False) |
|
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')
|
|
|