fix: Windows Playwright HTML rendering under Streamlit
Streamlit/Tornado switches the asyncio policy to WindowsSelectorEventLoopPolicy on Windows, which breaks Playwright subprocess creation. Fixed by using an explicit ProactorEventLoop on Windows, and tracking the event loop per browser instance to detect cross-loop reuse.
This commit is contained in:
1 parent
755fc3593e
commit
af52cf674a
2 files changed
+42
-3
No files matched your search
@@ -25,6 +25,7 @@ Linux Environment Requirements:
|
||||
Playwright browser install: playwright install --with-deps chromium
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
@@ -55,6 +56,7 @@ class HTMLFrameGenerator:
|
||||
|
||||
_browser = None
|
||||
_playwright = None
|
||||
_browser_loop = None
|
||||
|
||||
def __init__(self, template_path: str):
|
||||
"""
|
||||
@@ -307,7 +309,22 @@ class HTMLFrameGenerator:
|
||||
@classmethod
|
||||
async def _ensure_browser(cls):
|
||||
"""Lazily initialize a shared Playwright browser instance"""
|
||||
if cls._browser is None or not cls._browser.is_connected():
|
||||
current_loop = asyncio.get_running_loop()
|
||||
browser_usable = (
|
||||
cls._browser is not None
|
||||
and cls._browser_loop is current_loop
|
||||
and cls._browser.is_connected()
|
||||
)
|
||||
|
||||
if not browser_usable:
|
||||
if cls._browser is not None and cls._browser_loop is not current_loop:
|
||||
logger.warning(
|
||||
"Detected cross-loop Playwright browser reuse attempt; "
|
||||
"recreating browser for current event loop"
|
||||
)
|
||||
|
||||
cls._browser = None
|
||||
cls._playwright = None
|
||||
from playwright.async_api import async_playwright
|
||||
cls._playwright = await async_playwright().start()
|
||||
cls._browser = await cls._playwright.chromium.launch(
|
||||
@@ -318,6 +335,7 @@ class HTMLFrameGenerator:
|
||||
'--disable-extensions',
|
||||
]
|
||||
)
|
||||
cls._browser_loop = current_loop
|
||||
logger.debug("Initialized Playwright Chromium browser")
|
||||
return cls._browser
|
||||
|
||||
@@ -327,6 +345,7 @@ class HTMLFrameGenerator:
|
||||
if cls._browser:
|
||||
await cls._browser.close()
|
||||
cls._browser = None
|
||||
cls._browser_loop = None
|
||||
if cls._playwright:
|
||||
await cls._playwright.stop()
|
||||
cls._playwright = None
|
||||
@@ -410,5 +429,7 @@ class HTMLFrameGenerator:
|
||||
return output_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to render HTML template: {e}")
|
||||
raise RuntimeError(f"HTML rendering failed: {e}")
|
||||
logger.exception("Failed to render HTML template")
|
||||
raise RuntimeError(
|
||||
f"HTML rendering failed: {type(e).__name__}: {e}"
|
||||
) from e
|
||||
@@ -15,6 +15,7 @@ Async helper functions for web UI
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
@@ -23,6 +24,23 @@ from loguru import logger
|
||||
|
||||
def run_async(coro):
|
||||
"""Run async coroutine in sync context"""
|
||||
if sys.platform == "win32":
|
||||
# Streamlit/Tornado may switch the global asyncio policy to
|
||||
# WindowsSelectorEventLoopPolicy, which breaks subprocess-based
|
||||
# libraries such as Playwright on Windows. Use an explicit
|
||||
# Proactor loop here so this sync bridge does not depend on the
|
||||
# ambient global policy.
|
||||
loop = asyncio.ProactorEventLoop()
|
||||
try:
|
||||
return loop.run_until_complete(coro)
|
||||
finally:
|
||||
try:
|
||||
from pixelle_video.services.frame_html import HTMLFrameGenerator
|
||||
|
||||
loop.run_until_complete(HTMLFrameGenerator.close_browser())
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to cleanup HTML frame browser before loop close: {e}")
|
||||
loop.close()
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user