fastled 1.3.30__py3-none-any.whl → 1.4.50__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastled/__init__.py +30 -2
- fastled/__main__.py +14 -0
- fastled/__version__.py +1 -1
- fastled/app.py +51 -2
- fastled/args.py +33 -0
- fastled/client_server.py +188 -40
- fastled/compile_server.py +10 -0
- fastled/compile_server_impl.py +34 -1
- fastled/docker_manager.py +56 -14
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +6 -3
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/live_client.py +21 -1
- fastled/open_browser.py +84 -16
- fastled/parse_args.py +110 -9
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -52
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -19
- fastled/server_flask.py +37 -1
- fastled/settings.py +47 -3
- fastled/sketch.py +121 -4
- fastled/string_diff.py +162 -26
- fastled/test/examples.py +7 -5
- fastled/types.py +4 -0
- fastled/util.py +34 -0
- fastled/version.py +41 -41
- fastled/web_compile.py +379 -236
- fastled/zip_files.py +76 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -508
- fastled-1.4.50.dist-info/RECORD +60 -0
- fastled-1.3.30.dist-info/RECORD +0 -44
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/WHEEL +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Playwright browser integration for FastLED WASM compiler.
|
|
3
|
+
|
|
4
|
+
This module provides a Playwright-based browser implementation that can be used
|
|
5
|
+
in a Playwright browser instead of the default system browser when
|
|
6
|
+
Playwright is available.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import warnings
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from fastled.playwright.resize_tracking import ResizeTracker
|
|
18
|
+
|
|
19
|
+
# Set custom Playwright browser installation path
|
|
20
|
+
PLAYWRIGHT_DIR = Path.home() / ".fastled" / "playwright"
|
|
21
|
+
PLAYWRIGHT_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_chromium_executable_path() -> str | None:
|
|
25
|
+
"""Get the path to the custom Chromium executable if it exists."""
|
|
26
|
+
import glob
|
|
27
|
+
import platform
|
|
28
|
+
|
|
29
|
+
playwright_dir = PLAYWRIGHT_DIR
|
|
30
|
+
|
|
31
|
+
if platform.system() == "Windows":
|
|
32
|
+
chromium_pattern = str(
|
|
33
|
+
playwright_dir / "chromium-*" / "chrome-win" / "chrome.exe"
|
|
34
|
+
)
|
|
35
|
+
elif platform.system() == "Darwin": # macOS
|
|
36
|
+
chromium_pattern = str(
|
|
37
|
+
playwright_dir
|
|
38
|
+
/ "chromium-*"
|
|
39
|
+
/ "chrome-mac"
|
|
40
|
+
/ "Chromium.app"
|
|
41
|
+
/ "Contents"
|
|
42
|
+
/ "MacOS"
|
|
43
|
+
/ "Chromium"
|
|
44
|
+
)
|
|
45
|
+
else: # Linux
|
|
46
|
+
chromium_pattern = str(
|
|
47
|
+
playwright_dir / "chromium-*" / "chrome-linux" / "chrome"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
matches = glob.glob(chromium_pattern)
|
|
51
|
+
return matches[0] if matches else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PlaywrightBrowser:
|
|
55
|
+
"""Playwright browser manager for FastLED sketches."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, headless: bool = False, enable_extensions: bool = True):
|
|
58
|
+
"""Initialize the Playwright browser manager.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
headless: Whether to run the browser in headless mode
|
|
62
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
self.headless = headless
|
|
66
|
+
self.enable_extensions = enable_extensions
|
|
67
|
+
self.auto_resize = True # Always enable auto-resize
|
|
68
|
+
self.browser: Any = None
|
|
69
|
+
self.context: Any = None
|
|
70
|
+
self.page: Any = None
|
|
71
|
+
self.playwright: Any = None
|
|
72
|
+
self._should_exit = asyncio.Event()
|
|
73
|
+
self._extensions_dir: Path | None = None
|
|
74
|
+
self.resize_tracker: ResizeTracker | None = None
|
|
75
|
+
|
|
76
|
+
# Initialize extensions if enabled
|
|
77
|
+
if self.enable_extensions:
|
|
78
|
+
self._setup_extensions()
|
|
79
|
+
|
|
80
|
+
def _setup_extensions(self) -> None:
|
|
81
|
+
"""Setup Chrome extensions for enhanced debugging."""
|
|
82
|
+
try:
|
|
83
|
+
from fastled.playwright.chrome_extension_downloader import (
|
|
84
|
+
download_cpp_devtools_extension,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
extension_path = download_cpp_devtools_extension()
|
|
88
|
+
if extension_path and extension_path.exists():
|
|
89
|
+
self._extensions_dir = extension_path
|
|
90
|
+
print(
|
|
91
|
+
f"[PYTHON] C++ DevTools Support extension ready: {extension_path}"
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
print("[PYTHON] Warning: C++ DevTools Support extension not available")
|
|
95
|
+
self.enable_extensions = False
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"[PYTHON] Warning: Failed to setup Chrome extensions: {e}")
|
|
98
|
+
self.enable_extensions = False
|
|
99
|
+
|
|
100
|
+
def _detect_device_scale_factor(self) -> float | None:
|
|
101
|
+
"""Detect the system's device scale factor for natural browser behavior.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The detected device scale factor, or None if detection fails or
|
|
105
|
+
the value is outside reasonable bounds (0.5-4.0).
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
import tkinter
|
|
109
|
+
|
|
110
|
+
root = tkinter.Tk()
|
|
111
|
+
root.withdraw() # Hide the window
|
|
112
|
+
scale_factor = root.winfo_fpixels("1i") / 72.0
|
|
113
|
+
root.destroy()
|
|
114
|
+
|
|
115
|
+
# Validate the scale factor is in a reasonable range
|
|
116
|
+
if 0.5 <= scale_factor <= 4.0:
|
|
117
|
+
return scale_factor
|
|
118
|
+
else:
|
|
119
|
+
print(
|
|
120
|
+
f"[PYTHON] Detected scale factor {scale_factor:.2f} is outside reasonable bounds (0.5-4.0)"
|
|
121
|
+
)
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"[PYTHON] Could not detect device scale factor: {e}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
async def start(self) -> None:
|
|
129
|
+
"""Start the Playwright browser."""
|
|
130
|
+
if self.browser is None and self.context is None:
|
|
131
|
+
|
|
132
|
+
from playwright.async_api import async_playwright
|
|
133
|
+
|
|
134
|
+
self.playwright = async_playwright()
|
|
135
|
+
playwright = await self.playwright.start()
|
|
136
|
+
|
|
137
|
+
if self.enable_extensions and self._extensions_dir:
|
|
138
|
+
# Use persistent context for extensions
|
|
139
|
+
user_data_dir = PLAYWRIGHT_DIR / "user-data"
|
|
140
|
+
user_data_dir.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
|
|
142
|
+
launch_kwargs = {
|
|
143
|
+
"headless": False, # Extensions require headed mode
|
|
144
|
+
"channel": "chromium", # Required for extensions
|
|
145
|
+
"args": [
|
|
146
|
+
"--disable-dev-shm-usage",
|
|
147
|
+
"--disable-web-security",
|
|
148
|
+
"--allow-running-insecure-content",
|
|
149
|
+
f"--disable-extensions-except={self._extensions_dir}",
|
|
150
|
+
f"--load-extension={self._extensions_dir}",
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Get custom Chromium executable path if available
|
|
155
|
+
executable_path = get_chromium_executable_path()
|
|
156
|
+
if executable_path:
|
|
157
|
+
launch_kwargs["executable_path"] = executable_path
|
|
158
|
+
print(
|
|
159
|
+
f"[PYTHON] Using custom Chromium executable: {executable_path}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self.context = await playwright.chromium.launch_persistent_context(
|
|
163
|
+
str(user_data_dir), **launch_kwargs
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
print(
|
|
167
|
+
"[PYTHON] Started Playwright browser with C++ DevTools Support extension"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
else:
|
|
171
|
+
# Regular browser launch without extensions
|
|
172
|
+
executable_path = get_chromium_executable_path()
|
|
173
|
+
launch_kwargs = {
|
|
174
|
+
"headless": self.headless,
|
|
175
|
+
"args": [
|
|
176
|
+
"--disable-dev-shm-usage",
|
|
177
|
+
"--disable-web-security",
|
|
178
|
+
"--allow-running-insecure-content",
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if executable_path:
|
|
183
|
+
launch_kwargs["executable_path"] = executable_path
|
|
184
|
+
print(
|
|
185
|
+
f"[PYTHON] Using custom Chromium executable: {executable_path}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self.browser = await playwright.chromium.launch(**launch_kwargs)
|
|
189
|
+
|
|
190
|
+
if self.page is None:
|
|
191
|
+
if self.context:
|
|
192
|
+
# Using persistent context (with extensions)
|
|
193
|
+
if len(self.context.pages) > 0:
|
|
194
|
+
self.page = self.context.pages[0]
|
|
195
|
+
else:
|
|
196
|
+
self.page = await self.context.new_page()
|
|
197
|
+
elif self.browser:
|
|
198
|
+
# Using regular browser
|
|
199
|
+
# Detect system device scale factor for natural browser behavior
|
|
200
|
+
device_scale_factor = self._detect_device_scale_factor()
|
|
201
|
+
|
|
202
|
+
# Create browser context with detected or default device scale factor
|
|
203
|
+
if device_scale_factor:
|
|
204
|
+
context = await self.browser.new_context(
|
|
205
|
+
device_scale_factor=device_scale_factor
|
|
206
|
+
)
|
|
207
|
+
print(
|
|
208
|
+
f"[PYTHON] Created browser context with device scale factor: {device_scale_factor:.2f}"
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
context = await self.browser.new_context()
|
|
212
|
+
print(
|
|
213
|
+
"[PYTHON] Created browser context with default device scale factor"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self.page = await context.new_page()
|
|
217
|
+
|
|
218
|
+
async def open_url(self, url: str) -> None:
|
|
219
|
+
"""Open a URL in the Playwright browser.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
url: The URL to open
|
|
223
|
+
"""
|
|
224
|
+
if self.page is None:
|
|
225
|
+
await self.start()
|
|
226
|
+
|
|
227
|
+
print(f"Opening FastLED sketch in Playwright browser: {url}")
|
|
228
|
+
if self.page is not None:
|
|
229
|
+
await self.page.goto(url)
|
|
230
|
+
|
|
231
|
+
# Wait for the page to load
|
|
232
|
+
await self.page.wait_for_load_state("networkidle")
|
|
233
|
+
|
|
234
|
+
# Verify device scale factor is working correctly
|
|
235
|
+
try:
|
|
236
|
+
device_pixel_ratio = await self.page.evaluate("window.devicePixelRatio")
|
|
237
|
+
print(
|
|
238
|
+
f"[PYTHON] Verified browser device pixel ratio: {device_pixel_ratio}"
|
|
239
|
+
)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
print(f"[PYTHON] Could not verify device pixel ratio: {e}")
|
|
242
|
+
|
|
243
|
+
# Set up auto-resizing functionality if enabled
|
|
244
|
+
if self.auto_resize:
|
|
245
|
+
await self._setup_auto_resize()
|
|
246
|
+
|
|
247
|
+
# Check if C++ DevTools extension is loaded
|
|
248
|
+
if self.enable_extensions and self._extensions_dir:
|
|
249
|
+
try:
|
|
250
|
+
# Check if the extension is available in the DevTools
|
|
251
|
+
extensions_available = await self.page.evaluate(
|
|
252
|
+
"""
|
|
253
|
+
() => {
|
|
254
|
+
// Check if chrome.devtools is available (extension context)
|
|
255
|
+
return typeof chrome !== 'undefined' &&
|
|
256
|
+
typeof chrome.runtime !== 'undefined' &&
|
|
257
|
+
chrome.runtime.id !== undefined;
|
|
258
|
+
}
|
|
259
|
+
"""
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if extensions_available:
|
|
263
|
+
print(
|
|
264
|
+
"[PYTHON] ✅ C++ DevTools Support extension is active and ready for DWARF debugging"
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
print(
|
|
268
|
+
"[PYTHON] ⚠️ C++ DevTools Support extension may not be fully loaded"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
print(f"[PYTHON] Could not verify extension status: {e}")
|
|
273
|
+
|
|
274
|
+
async def _setup_auto_resize(self) -> None:
|
|
275
|
+
"""Set up automatic window resizing based on content size."""
|
|
276
|
+
if self.page is None:
|
|
277
|
+
print("[PYTHON] Cannot setup auto-resize: page is None")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
print(
|
|
281
|
+
"[PYTHON] Setting up browser window tracking with viewport-only adjustment"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Create resize tracker instance
|
|
285
|
+
self.resize_tracker = ResizeTracker(self.page)
|
|
286
|
+
|
|
287
|
+
# Start polling loop that tracks browser window changes and adjusts viewport only
|
|
288
|
+
asyncio.create_task(self._track_browser_adjust_viewport())
|
|
289
|
+
|
|
290
|
+
async def _track_browser_adjust_viewport(self) -> None:
|
|
291
|
+
"""Track browser window changes and adjust viewport accordingly.
|
|
292
|
+
|
|
293
|
+
This method polls for changes in the browser window size using the
|
|
294
|
+
ResizeTracker and handles any errors that occur.
|
|
295
|
+
"""
|
|
296
|
+
if self.resize_tracker is None:
|
|
297
|
+
print("[PYTHON] Cannot start tracking: resize_tracker is None")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
while not self._should_exit.is_set():
|
|
301
|
+
try:
|
|
302
|
+
# Wait between polls
|
|
303
|
+
await asyncio.sleep(0.25) # Poll every 250ms
|
|
304
|
+
|
|
305
|
+
# Check if page is still alive
|
|
306
|
+
if self.page is None or self.page.is_closed():
|
|
307
|
+
print("[PYTHON] Page closed, signaling exit")
|
|
308
|
+
self._should_exit.set()
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# Update resize tracking
|
|
312
|
+
result = await self.resize_tracker.update()
|
|
313
|
+
|
|
314
|
+
if result is not None:
|
|
315
|
+
assert isinstance(result, Exception)
|
|
316
|
+
# An exception occurred in resize tracking
|
|
317
|
+
error_message = str(result)
|
|
318
|
+
warnings.warn(f"[PYTHON] Error in resize tracking: {error_message}")
|
|
319
|
+
|
|
320
|
+
# Be EXTREMELY conservative about browser close detection
|
|
321
|
+
# Only trigger shutdown on very specific errors that definitively indicate browser closure
|
|
322
|
+
browser_definitely_closed = any(
|
|
323
|
+
phrase in error_message.lower()
|
|
324
|
+
for phrase in [
|
|
325
|
+
"browser has been closed",
|
|
326
|
+
"target closed",
|
|
327
|
+
"connection closed",
|
|
328
|
+
"target page, probably because the page has been closed",
|
|
329
|
+
"page has been closed",
|
|
330
|
+
"browser context has been closed",
|
|
331
|
+
]
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Also check actual browser state before deciding to shut down
|
|
335
|
+
browser_state_indicates_closed = False
|
|
336
|
+
try:
|
|
337
|
+
if self.browser and hasattr(self.browser, "is_closed"):
|
|
338
|
+
browser_state_indicates_closed = self.browser.is_closed()
|
|
339
|
+
elif self.context and hasattr(self.context, "closed"):
|
|
340
|
+
browser_state_indicates_closed = self.context.closed
|
|
341
|
+
except Exception:
|
|
342
|
+
# If we can't check the state, don't assume it's closed
|
|
343
|
+
warnings.warn(
|
|
344
|
+
f"[PYTHON] Could not check browser state: {result}. Assuming browser is not closed."
|
|
345
|
+
)
|
|
346
|
+
browser_state_indicates_closed = False
|
|
347
|
+
|
|
348
|
+
if browser_definitely_closed or browser_state_indicates_closed:
|
|
349
|
+
if browser_definitely_closed:
|
|
350
|
+
print(
|
|
351
|
+
f'[PYTHON] Browser has been closed because "{error_message}" matched one of the error phrases or browser state indicates closed, shutting down gracefully...'
|
|
352
|
+
)
|
|
353
|
+
elif browser_state_indicates_closed:
|
|
354
|
+
print(
|
|
355
|
+
"[PYTHON] Browser state indicates closed, shutting down gracefully..."
|
|
356
|
+
)
|
|
357
|
+
self._should_exit.set()
|
|
358
|
+
return
|
|
359
|
+
else:
|
|
360
|
+
# For other errors, just log and continue - don't shut down
|
|
361
|
+
print(
|
|
362
|
+
f"[PYTHON] Recoverable error in resize tracking: {result}"
|
|
363
|
+
)
|
|
364
|
+
# Add a small delay to prevent tight error loops
|
|
365
|
+
await asyncio.sleep(1.0)
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
error_message = str(e)
|
|
369
|
+
warnings.warn(
|
|
370
|
+
f"[PYTHON] Unexpected error in browser tracking loop: {error_message}"
|
|
371
|
+
)
|
|
372
|
+
# Add a small delay to prevent tight error loops
|
|
373
|
+
await asyncio.sleep(1.0)
|
|
374
|
+
|
|
375
|
+
warnings.warn("[PYTHON] Browser tracking loop exited.")
|
|
376
|
+
|
|
377
|
+
async def wait_for_close(self) -> None:
|
|
378
|
+
"""Wait for the browser to be closed."""
|
|
379
|
+
if self.context:
|
|
380
|
+
# Wait for persistent context to be closed
|
|
381
|
+
try:
|
|
382
|
+
while not self.context.closed:
|
|
383
|
+
await asyncio.sleep(1)
|
|
384
|
+
except Exception:
|
|
385
|
+
pass
|
|
386
|
+
elif self.browser:
|
|
387
|
+
try:
|
|
388
|
+
# Wait for the browser to be closed
|
|
389
|
+
while not self.browser.is_closed():
|
|
390
|
+
await asyncio.sleep(1)
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
async def close(self) -> None:
|
|
395
|
+
"""Close the Playwright browser."""
|
|
396
|
+
# Signal all tracking loops to exit
|
|
397
|
+
self._should_exit.set()
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
# Clean up resize tracker
|
|
401
|
+
self.resize_tracker = None
|
|
402
|
+
|
|
403
|
+
if self.page:
|
|
404
|
+
await self.page.close()
|
|
405
|
+
self.page = None
|
|
406
|
+
|
|
407
|
+
if self.context:
|
|
408
|
+
await self.context.close()
|
|
409
|
+
self.context = None
|
|
410
|
+
|
|
411
|
+
if self.browser:
|
|
412
|
+
await self.browser.close()
|
|
413
|
+
self.browser = None
|
|
414
|
+
|
|
415
|
+
if self.playwright:
|
|
416
|
+
# The playwright context manager may not have a stop() method in all versions
|
|
417
|
+
# Try stop() first, fall back to __aexit__ if needed
|
|
418
|
+
try:
|
|
419
|
+
if hasattr(self.playwright, "stop"):
|
|
420
|
+
await self.playwright.stop()
|
|
421
|
+
else:
|
|
422
|
+
# For async context managers, use __aexit__
|
|
423
|
+
await self.playwright.__aexit__(None, None, None)
|
|
424
|
+
except Exception as stop_error:
|
|
425
|
+
print(
|
|
426
|
+
f"[PYTHON] Warning: Could not properly stop playwright: {stop_error}"
|
|
427
|
+
)
|
|
428
|
+
# Try alternative cleanup methods
|
|
429
|
+
try:
|
|
430
|
+
if hasattr(self.playwright, "__aexit__"):
|
|
431
|
+
await self.playwright.__aexit__(None, None, None)
|
|
432
|
+
except Exception:
|
|
433
|
+
pass # Ignore secondary cleanup failures
|
|
434
|
+
finally:
|
|
435
|
+
self.playwright = None
|
|
436
|
+
except KeyboardInterrupt:
|
|
437
|
+
print("[PYTHON] Keyboard interrupt detected, closing Playwright browser")
|
|
438
|
+
self._should_exit.set()
|
|
439
|
+
import _thread
|
|
440
|
+
|
|
441
|
+
_thread.interrupt_main()
|
|
442
|
+
except Exception as e:
|
|
443
|
+
print(f"[PYTHON] Error closing Playwright browser: {e}")
|
|
444
|
+
self._should_exit.set()
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def run_playwright_browser(
|
|
448
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Run Playwright browser in a separate process.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
url: The URL to open
|
|
454
|
+
headless: Whether to run in headless mode
|
|
455
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
async def main():
|
|
459
|
+
browser = None
|
|
460
|
+
try:
|
|
461
|
+
browser = PlaywrightBrowser(
|
|
462
|
+
headless=headless, enable_extensions=enable_extensions
|
|
463
|
+
)
|
|
464
|
+
await browser.start()
|
|
465
|
+
await browser.open_url(url)
|
|
466
|
+
|
|
467
|
+
print("Playwright browser opened. Press Ctrl+C to close.")
|
|
468
|
+
await browser.wait_for_close()
|
|
469
|
+
|
|
470
|
+
except Exception as e:
|
|
471
|
+
# If we get an error that suggests browsers aren't installed, try to install them
|
|
472
|
+
if "executable doesn't exist" in str(e) or "Browser not found" in str(e):
|
|
473
|
+
print("🎭 Playwright browsers not found. Installing...")
|
|
474
|
+
if install_playwright_browsers():
|
|
475
|
+
print("🎭 Retrying browser startup...")
|
|
476
|
+
# Try again with fresh browser instance
|
|
477
|
+
browser = PlaywrightBrowser(
|
|
478
|
+
headless=headless, enable_extensions=enable_extensions
|
|
479
|
+
)
|
|
480
|
+
await browser.start()
|
|
481
|
+
await browser.open_url(url)
|
|
482
|
+
|
|
483
|
+
print("Playwright browser opened. Press Ctrl+C to close.")
|
|
484
|
+
await browser.wait_for_close()
|
|
485
|
+
else:
|
|
486
|
+
print("❌ Failed to install Playwright browsers")
|
|
487
|
+
raise e
|
|
488
|
+
else:
|
|
489
|
+
raise e
|
|
490
|
+
except KeyboardInterrupt:
|
|
491
|
+
print("\nClosing Playwright browser...")
|
|
492
|
+
finally:
|
|
493
|
+
if browser is not None:
|
|
494
|
+
try:
|
|
495
|
+
await browser.close()
|
|
496
|
+
except Exception as e:
|
|
497
|
+
print(f"Warning: Failed to close Playwright browser: {e}")
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
asyncio.run(main())
|
|
501
|
+
except KeyboardInterrupt:
|
|
502
|
+
print("\nPlaywright browser closed.")
|
|
503
|
+
except Exception as e:
|
|
504
|
+
print(f"Playwright browser failed: {e}. Falling back to default browser.")
|
|
505
|
+
import webbrowser
|
|
506
|
+
|
|
507
|
+
webbrowser.open(url)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class PlaywrightBrowserProxy:
|
|
511
|
+
"""Proxy object to manage Playwright browser lifecycle."""
|
|
512
|
+
|
|
513
|
+
def __init__(self):
|
|
514
|
+
self.process = None
|
|
515
|
+
self.browser_manager = None
|
|
516
|
+
self.monitor_thread = None
|
|
517
|
+
self._closing_intentionally = False
|
|
518
|
+
|
|
519
|
+
def open(
|
|
520
|
+
self, url: str, headless: bool = False, enable_extensions: bool = True
|
|
521
|
+
) -> None:
|
|
522
|
+
"""Open URL with Playwright browser and keep it alive.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
url: The URL to open
|
|
526
|
+
headless: Whether to run in headless mode
|
|
527
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
# Run Playwright in a separate process to avoid blocking
|
|
532
|
+
import multiprocessing
|
|
533
|
+
|
|
534
|
+
self.process = multiprocessing.Process(
|
|
535
|
+
target=run_playwright_browser_persistent,
|
|
536
|
+
args=(url, headless, enable_extensions),
|
|
537
|
+
)
|
|
538
|
+
self.process.start()
|
|
539
|
+
|
|
540
|
+
# Start monitoring thread to exit main process when browser subprocess exits
|
|
541
|
+
self._start_monitor_thread()
|
|
542
|
+
|
|
543
|
+
# Register cleanup
|
|
544
|
+
import atexit
|
|
545
|
+
|
|
546
|
+
atexit.register(self.close)
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
warnings.warn(
|
|
550
|
+
f"Failed to start Playwright browser: {e}. Falling back to default browser."
|
|
551
|
+
)
|
|
552
|
+
import webbrowser
|
|
553
|
+
|
|
554
|
+
webbrowser.open(url)
|
|
555
|
+
|
|
556
|
+
def _start_monitor_thread(self) -> None:
|
|
557
|
+
"""Start a thread to monitor the browser process and exit main process when it terminates."""
|
|
558
|
+
if self.monitor_thread is not None:
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
def monitor_process():
|
|
562
|
+
"""Monitor the browser process and exit when it terminates."""
|
|
563
|
+
if self.process is None:
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
# Wait for the process to terminate
|
|
568
|
+
self.process.join()
|
|
569
|
+
|
|
570
|
+
# Check if the process terminated (and we didn't kill it ourselves)
|
|
571
|
+
if (
|
|
572
|
+
self.process.exitcode is not None
|
|
573
|
+
and not self._closing_intentionally
|
|
574
|
+
):
|
|
575
|
+
print("[MAIN] Browser closed, exiting main program")
|
|
576
|
+
# Force exit the entire program
|
|
577
|
+
os._exit(0)
|
|
578
|
+
|
|
579
|
+
except KeyboardInterrupt:
|
|
580
|
+
print("[MAIN] Browser monitor interrupted by user")
|
|
581
|
+
import _thread
|
|
582
|
+
|
|
583
|
+
_thread.interrupt_main()
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print(f"[MAIN] Error monitoring browser process: {e}")
|
|
586
|
+
|
|
587
|
+
self.monitor_thread = threading.Thread(target=monitor_process, daemon=True)
|
|
588
|
+
self.monitor_thread.start()
|
|
589
|
+
|
|
590
|
+
def close(self) -> None:
|
|
591
|
+
"""Close the Playwright browser."""
|
|
592
|
+
if self.process and self.process.is_alive():
|
|
593
|
+
print("Closing Playwright browser...")
|
|
594
|
+
# Mark that we're intentionally closing to prevent monitor from triggering exit
|
|
595
|
+
self._closing_intentionally = True
|
|
596
|
+
self.process.terminate()
|
|
597
|
+
self.process.join(timeout=5)
|
|
598
|
+
if self.process.is_alive():
|
|
599
|
+
self.process.kill()
|
|
600
|
+
self.process = None
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def run_playwright_browser_persistent(
|
|
604
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
|
605
|
+
) -> None:
|
|
606
|
+
"""Run Playwright browser in a persistent mode that stays alive until terminated.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
url: The URL to open
|
|
610
|
+
headless: Whether to run in headless mode
|
|
611
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
async def main():
|
|
615
|
+
browser = None
|
|
616
|
+
try:
|
|
617
|
+
browser = PlaywrightBrowser(
|
|
618
|
+
headless=headless, enable_extensions=enable_extensions
|
|
619
|
+
)
|
|
620
|
+
await browser.start()
|
|
621
|
+
await browser.open_url(url)
|
|
622
|
+
|
|
623
|
+
print(
|
|
624
|
+
"Playwright browser opened. Browser will remain open until the FastLED process exits."
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# Keep the browser alive until exit is signaled
|
|
628
|
+
while not browser._should_exit.is_set():
|
|
629
|
+
await asyncio.sleep(0.1)
|
|
630
|
+
|
|
631
|
+
except Exception as e:
|
|
632
|
+
# If we get an error that suggests browsers aren't installed, try to install them
|
|
633
|
+
if "executable doesn't exist" in str(e) or "Browser not found" in str(e):
|
|
634
|
+
print("🎭 Playwright browsers not found. Installing...")
|
|
635
|
+
if install_playwright_browsers():
|
|
636
|
+
print("🎭 Retrying browser startup...")
|
|
637
|
+
# Try again with fresh browser instance
|
|
638
|
+
browser = PlaywrightBrowser(
|
|
639
|
+
headless=headless, enable_extensions=enable_extensions
|
|
640
|
+
)
|
|
641
|
+
await browser.start()
|
|
642
|
+
await browser.open_url(url)
|
|
643
|
+
|
|
644
|
+
print(
|
|
645
|
+
"Playwright browser opened. Browser will remain open until the FastLED process exits."
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Keep the browser alive until exit is signaled
|
|
649
|
+
while not browser._should_exit.is_set():
|
|
650
|
+
await asyncio.sleep(0.1)
|
|
651
|
+
else:
|
|
652
|
+
print("❌ Failed to install Playwright browsers")
|
|
653
|
+
raise e
|
|
654
|
+
else:
|
|
655
|
+
raise e
|
|
656
|
+
except KeyboardInterrupt:
|
|
657
|
+
print("\nClosing Playwright browser...")
|
|
658
|
+
finally:
|
|
659
|
+
if browser is not None:
|
|
660
|
+
try:
|
|
661
|
+
await browser.close()
|
|
662
|
+
except Exception as e:
|
|
663
|
+
print(f"Warning: Failed to close Playwright browser: {e}")
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
asyncio.run(main())
|
|
667
|
+
except KeyboardInterrupt:
|
|
668
|
+
print("\nPlaywright browser closed.")
|
|
669
|
+
except Exception as e:
|
|
670
|
+
print(f"Playwright browser failed: {e}")
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def open_with_playwright(
|
|
674
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
|
675
|
+
) -> PlaywrightBrowserProxy:
|
|
676
|
+
"""Open URL with Playwright browser and return a proxy object for lifecycle management.
|
|
677
|
+
|
|
678
|
+
This function can be used as a drop-in replacement for webbrowser.open().
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
url: The URL to open
|
|
682
|
+
headless: Whether to run in headless mode
|
|
683
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
PlaywrightBrowserProxy object for managing the browser lifecycle
|
|
687
|
+
"""
|
|
688
|
+
proxy = PlaywrightBrowserProxy()
|
|
689
|
+
proxy.open(url, headless, enable_extensions)
|
|
690
|
+
return proxy
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def install_playwright_browsers() -> bool:
|
|
694
|
+
"""Install Playwright browsers if not already installed.
|
|
695
|
+
|
|
696
|
+
Installs browsers to ~/.fastled/playwright directory.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
True if installation was successful or browsers were already installed
|
|
700
|
+
"""
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
import os
|
|
704
|
+
from pathlib import Path
|
|
705
|
+
|
|
706
|
+
# Set custom browser installation path
|
|
707
|
+
playwright_dir = Path.home() / ".fastled" / "playwright"
|
|
708
|
+
playwright_dir.mkdir(parents=True, exist_ok=True)
|
|
709
|
+
|
|
710
|
+
# Check if browsers are installed in the custom directory
|
|
711
|
+
import glob
|
|
712
|
+
import platform
|
|
713
|
+
|
|
714
|
+
if platform.system() == "Windows":
|
|
715
|
+
chromium_pattern = str(
|
|
716
|
+
playwright_dir / "chromium-*" / "chrome-win" / "chrome.exe"
|
|
717
|
+
)
|
|
718
|
+
elif platform.system() == "Darwin": # macOS
|
|
719
|
+
chromium_pattern = str(
|
|
720
|
+
playwright_dir / "chromium-*" / "chrome-mac" / "Chromium.app"
|
|
721
|
+
)
|
|
722
|
+
else: # Linux
|
|
723
|
+
chromium_pattern = str(
|
|
724
|
+
playwright_dir / "chromium-*" / "chrome-linux" / "chrome"
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
if glob.glob(chromium_pattern):
|
|
728
|
+
print(f"✅ Playwright browsers already installed at: {playwright_dir}")
|
|
729
|
+
return True
|
|
730
|
+
|
|
731
|
+
# If we get here, browsers need to be installed
|
|
732
|
+
print("Installing Playwright browsers...")
|
|
733
|
+
print(f"Installing to: {playwright_dir}")
|
|
734
|
+
import subprocess
|
|
735
|
+
|
|
736
|
+
env = dict(os.environ)
|
|
737
|
+
env["PLAYWRIGHT_BROWSERS_PATH"] = str(playwright_dir.resolve())
|
|
738
|
+
|
|
739
|
+
result = subprocess.run(
|
|
740
|
+
[sys.executable, "-m", "playwright", "install", "chromium"],
|
|
741
|
+
env=env,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
if result.returncode == 0:
|
|
745
|
+
print("✅ Playwright browsers installed successfully!")
|
|
746
|
+
print(f" Location: {playwright_dir}")
|
|
747
|
+
|
|
748
|
+
# Also download the C++ DevTools Support extension
|
|
749
|
+
try:
|
|
750
|
+
from fastled.playwright.chrome_extension_downloader import (
|
|
751
|
+
download_cpp_devtools_extension,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
extension_path = download_cpp_devtools_extension()
|
|
755
|
+
if extension_path:
|
|
756
|
+
print(
|
|
757
|
+
"✅ C++ DevTools Support extension ready for DWARF debugging!"
|
|
758
|
+
)
|
|
759
|
+
else:
|
|
760
|
+
print("⚠️ C++ DevTools Support extension download failed")
|
|
761
|
+
except Exception as e:
|
|
762
|
+
print(f"⚠️ Failed to setup C++ DevTools Support extension: {e}")
|
|
763
|
+
|
|
764
|
+
return True
|
|
765
|
+
else:
|
|
766
|
+
print(
|
|
767
|
+
f"❌ Failed to install Playwright browsers (exit code: {result.returncode})"
|
|
768
|
+
)
|
|
769
|
+
return False
|
|
770
|
+
|
|
771
|
+
except Exception as e:
|
|
772
|
+
print(f"❌ Error installing Playwright browsers: {e}")
|
|
773
|
+
return False
|