-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.py
More file actions
110 lines (86 loc) · 2.89 KB
/
run.py
File metadata and controls
110 lines (86 loc) · 2.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""GridRunner desktop entry point.
Starts the FastAPI server in a background thread and opens
a native OS webview window via pywebview.
"""
import os
import socket
import threading
import time
import httpx
import uvicorn
import webview
def find_free_port() -> int:
"""Find an available port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def wait_for_server(port: int, timeout: float = 10.0) -> None:
"""Poll the health endpoint until the server is ready."""
url = f"http://127.0.0.1:{port}/health"
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
resp = httpx.get(url, timeout=1.0)
if resp.status_code == 200:
return
except httpx.ConnectError:
pass
time.sleep(0.1)
raise TimeoutError(f"Server did not start within {timeout}s")
class JsApi:
"""Python API exposed to the frontend via window.pywebview.api."""
def __init__(self, window_ref):
self._window_ref = window_ref
def browse_file(self, file_types=None):
"""Open a native file picker and return the selected path."""
result = self._window_ref().create_file_dialog(
webview.OPEN_DIALOG,
file_types=file_types or ("All files (*.*)",),
)
if result and len(result) > 0:
return result[0]
return None
def browse_directory(self):
"""Open a native folder picker and return the selected path."""
result = self._window_ref().create_file_dialog(
webview.FOLDER_DIALOG,
)
if result and len(result) > 0:
return result[0]
return None
def main() -> None:
port = find_free_port()
# Set port before importing backend (config reads env at import time)
os.environ["GRIDRUNNER_PORT"] = str(port)
from backend.main import app
config = uvicorn.Config(
app,
host="127.0.0.1",
port=port,
log_level="warning",
)
server = uvicorn.Server(config)
# Run uvicorn in a daemon thread (its own asyncio loop)
server_thread = threading.Thread(target=server.run, daemon=True)
server_thread.start()
wait_for_server(port)
# Use a mutable container so JsApi can reference the window after creation
window_holder = []
js_api = JsApi(lambda: window_holder[0])
# pywebview owns the main thread (native GUI event loop)
window = webview.create_window(
"GridRunner",
url=f"http://127.0.0.1:{port}",
width=1200,
height=800,
resizable=True,
min_size=(800, 600),
js_api=js_api,
)
window_holder.append(window)
webview.start()
# Window closed — shut down server
server.should_exit = True
server_thread.join(timeout=3)
if __name__ == "__main__":
main()