Spaces:
Paused
Paused
opencode commited on
Commit ·
f31486b
1
Parent(s): a1ec78c
桌面加入 Playwright Chromium:背景下載、桌面圖示與啟動器
Browse files- README.md +31 -2
- app.py +292 -1
- packages.txt +31 -0
- requirements.txt +1 -0
README.md
CHANGED
|
@@ -20,8 +20,9 @@ OpenCode 在啟動時安裝,並重設使用者層級設定為預設值。
|
|
| 20 |
## 檔案
|
| 21 |
|
| 22 |
- `app.py` — FastAPI + xterm.js 終端機、檔案瀏覽器、`/ws` session 管理與固定 LXDE 桌面
|
| 23 |
-
- `packages.txt` — 系統套件(`tmux`、`xvfb`、`x11vnc`、`lxde-core`、`xterm`、`novnc`、
|
| 24 |
-
-
|
|
|
|
| 25 |
|
| 26 |
## 終端機分頁(全部共用、全部長存)
|
| 27 |
|
|
@@ -66,6 +67,34 @@ OpenCode 在啟動時安裝,並重設使用者層級設定為預設值。
|
|
| 66 |
(預設 `1440x900x24`)、`DESKTOP_VNC_PORT`(預設 `5900`)、`DESKTOP_NOVNC_ROOT`
|
| 67 |
(預設 `/usr/share/novnc`)。noVNC 靜態檔由 `/novnc/*` 提供,缺少時前端自動改用 CDN。
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
## 記憶體守衛(memguard)
|
| 70 |
|
| 71 |
每次 Space 啟動時,`app.py` 的 `install_memguard()` 會自動:
|
|
|
|
| 20 |
## 檔案
|
| 21 |
|
| 22 |
- `app.py` — FastAPI + xterm.js 終端機、檔案瀏覽器、`/ws` session 管理與固定 LXDE 桌面
|
| 23 |
+
- `packages.txt` — 系統套件(`tmux`、`xvfb`、`x11vnc`、`lxde-core`、`xterm`、`novnc`、
|
| 24 |
+
`x11-utils`,以及 Playwright Chromium 所需的系統函式庫與字型)
|
| 25 |
+
- `requirements.txt` — Python 相依套件(含 `playwright`)
|
| 26 |
|
| 27 |
## 終端機分頁(全部共用、全部長存)
|
| 28 |
|
|
|
|
| 67 |
(預設 `1440x900x24`)、`DESKTOP_VNC_PORT`(預設 `5900`)、`DESKTOP_NOVNC_ROOT`
|
| 68 |
(預設 `/usr/share/novnc`)。noVNC 靜態檔由 `/novnc/*` 提供,缺少時前端自動改用 CDN。
|
| 69 |
|
| 70 |
+
## Playwright + Chromium(桌面內使用)
|
| 71 |
+
|
| 72 |
+
Space 啟動後會在背景自動安裝 Playwright 的 Chromium(下載到
|
| 73 |
+
`~/.cache/ms-playwright`,系統依賴已由 `packages.txt` 預裝),完成後:
|
| 74 |
+
|
| 75 |
+
- 共用 LXDE 桌面會出現 **Playwright Chromium** 圖示,點兩下即可在桌面內開瀏覽器
|
| 76 |
+
(以 root 執行,已自動加上 `--no-sandbox --disable-dev-shm-usage`)。
|
| 77 |
+
- 也可直接執行啟動器:`~/.local/bin/playwright-chromium`。
|
| 78 |
+
- 在終端機用 Python 操作時,記得 Chromium 需要無沙箱參數:
|
| 79 |
+
|
| 80 |
+
```python
|
| 81 |
+
from playwright.sync_api import sync_playwright
|
| 82 |
+
|
| 83 |
+
with sync_playwright() as p:
|
| 84 |
+
browser = p.chromium.launch(args=["--no-sandbox", "--disable-dev-shm-usage"])
|
| 85 |
+
page = browser.new_page()
|
| 86 |
+
page.goto("https://example.com")
|
| 87 |
+
print(page.title())
|
| 88 |
+
browser.close()
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
- 若要開在共用桌面(看的到視窗),把 `headless` 設為 `False` 並使用 `DISPLAY=:99`;
|
| 92 |
+
Playwright 會自動沿用 `PLAYWRIGHT_BROWSERS_PATH`。
|
| 93 |
+
- 除錯端點:`GET /api/playwright/status`(是否就緒、Chromium 路徑、最後錯誤)、
|
| 94 |
+
`POST /api/playwright/install`(手動觸發重裝)。安裝 log 在
|
| 95 |
+
`/tmp/webterm-playwright.log`。
|
| 96 |
+
- 手動更新 Chromium:`python3 -m playwright install chromium`。
|
| 97 |
+
|
| 98 |
## 記憶體守衛(memguard)
|
| 99 |
|
| 100 |
每次 Space 啟動時,`app.py` 的 `install_memguard()` 會自動:
|
app.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
import os
|
| 2 |
import pty
|
|
|
|
| 3 |
import shutil
|
| 4 |
import socket
|
| 5 |
import subprocess
|
|
|
|
| 6 |
import signal
|
| 7 |
import struct
|
| 8 |
import fcntl
|
|
@@ -1181,6 +1183,19 @@ DESKTOP_APT_PACKAGES = "xvfb x11vnc lxde-core xterm novnc x11-utils"
|
|
| 1181 |
DESKTOP_TERM_TITLE = "共用終端機"
|
| 1182 |
DESKTOP_WATCHDOG = None
|
| 1183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1184 |
DESKTOP_PAGE = r"""<!DOCTYPE html>
|
| 1185 |
<html lang="zh-TW">
|
| 1186 |
<head>
|
|
@@ -1824,6 +1839,276 @@ class DesktopManager:
|
|
| 1824 |
desktop_manager = DesktopManager()
|
| 1825 |
|
| 1826 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1827 |
async def desktop_watchdog():
|
| 1828 |
"""每 3 秒確認桌面還在,且永遠只有一份;不影響終端機流程。"""
|
| 1829 |
await asyncio.sleep(1)
|
|
@@ -1844,7 +2129,7 @@ def dummy_gpu():
|
|
| 1844 |
|
| 1845 |
@app.on_event("startup")
|
| 1846 |
async def startup_event():
|
| 1847 |
-
global DESKTOP_WATCHDOG
|
| 1848 |
_load_tabs()
|
| 1849 |
_write_shell_launcher()
|
| 1850 |
await ensure_tmux()
|
|
@@ -1855,6 +2140,12 @@ async def startup_event():
|
|
| 1855 |
DESKTOP_WATCHDOG = asyncio.create_task(desktop_watchdog())
|
| 1856 |
except Exception as _desktop_err: # pragma: no cover
|
| 1857 |
log.warning("[desktop] 無法啟動看門狗(終端機照常運作):%r", _desktop_err)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1858 |
dummy_gpu()
|
| 1859 |
if SKIP_INSTALL:
|
| 1860 |
log.info("WEBTERM_SKIP_INSTALL=1:跳過 opencode / 編輯器安裝")
|
|
|
|
| 1 |
import os
|
| 2 |
import pty
|
| 3 |
+
import shlex
|
| 4 |
import shutil
|
| 5 |
import socket
|
| 6 |
import subprocess
|
| 7 |
+
import sys
|
| 8 |
import signal
|
| 9 |
import struct
|
| 10 |
import fcntl
|
|
|
|
| 1183 |
DESKTOP_TERM_TITLE = "共用終端機"
|
| 1184 |
DESKTOP_WATCHDOG = None
|
| 1185 |
|
| 1186 |
+
# ──────────────────────────── Playwright + Chromium(桌面用)─────────────────
|
| 1187 |
+
# Chromium 由 Playwright 管理,安裝在 ~/.cache/ms-playwright;完成後在共用桌面
|
| 1188 |
+
# 建立「Playwright Chromium」圖示,並提供一支可直接執行的啟動器。
|
| 1189 |
+
PW_BROWSERS = os.path.join(HOME, ".cache", "ms-playwright")
|
| 1190 |
+
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", PW_BROWSERS)
|
| 1191 |
+
PW_LOG = "/tmp/webterm-playwright.log"
|
| 1192 |
+
PW_TITLE = "Playwright Chromium"
|
| 1193 |
+
PW_LAUNCHER = os.path.join(HOME, ".local", "bin", "playwright-chromium")
|
| 1194 |
+
PW_APP_ENTRY = os.path.join(HOME, ".local", "share", "applications", "playwright-chromium.desktop")
|
| 1195 |
+
PW_DESKTOP_ENTRY = os.path.join(HOME, "Desktop", "Playwright Chromium.desktop")
|
| 1196 |
+
PW_WATCHDOG = None
|
| 1197 |
+
PW_TASK = None
|
| 1198 |
+
|
| 1199 |
DESKTOP_PAGE = r"""<!DOCTYPE html>
|
| 1200 |
<html lang="zh-TW">
|
| 1201 |
<head>
|
|
|
|
| 1839 |
desktop_manager = DesktopManager()
|
| 1840 |
|
| 1841 |
|
| 1842 |
+
class PlaywrightManager:
|
| 1843 |
+
"""背景安裝 Playwright Chromium,完成後在共用桌面建立啟動圖示。"""
|
| 1844 |
+
|
| 1845 |
+
def __init__(self):
|
| 1846 |
+
self._lock = asyncio.Lock()
|
| 1847 |
+
self._logf = None
|
| 1848 |
+
self.ready = False
|
| 1849 |
+
self.installing = False
|
| 1850 |
+
self.chromium_path = None
|
| 1851 |
+
self.version = ""
|
| 1852 |
+
self.last_error = ""
|
| 1853 |
+
self.installed_at = 0.0
|
| 1854 |
+
self._last_attempt = 0.0
|
| 1855 |
+
|
| 1856 |
+
# ── 日誌 ──────────────────────────────────────────────────
|
| 1857 |
+
def _log(self, message: str, level: int = logging.INFO):
|
| 1858 |
+
line = f"[playwright] {message}"
|
| 1859 |
+
log.log(level, "%s", line)
|
| 1860 |
+
try:
|
| 1861 |
+
f = self._logfd()
|
| 1862 |
+
if hasattr(f, "write"):
|
| 1863 |
+
f.write(time.strftime("%Y-%m-%d %H:%M:%S ") + line + "\n")
|
| 1864 |
+
f.flush()
|
| 1865 |
+
except OSError:
|
| 1866 |
+
pass
|
| 1867 |
+
|
| 1868 |
+
def _logfd(self):
|
| 1869 |
+
if self._logf is None:
|
| 1870 |
+
try:
|
| 1871 |
+
self._logf = open(PW_LOG, "a", buffering=1)
|
| 1872 |
+
except OSError:
|
| 1873 |
+
self._logf = subprocess.DEVNULL
|
| 1874 |
+
return self._logf
|
| 1875 |
+
|
| 1876 |
+
# ── 執行工具 ──────────────────────────────────────────────
|
| 1877 |
+
@staticmethod
|
| 1878 |
+
def _env():
|
| 1879 |
+
env = os.environ.copy()
|
| 1880 |
+
env["PLAYWRIGHT_BROWSERS_PATH"] = PW_BROWSERS
|
| 1881 |
+
env["DEBIAN_FRONTEND"] = "noninteractive"
|
| 1882 |
+
return env
|
| 1883 |
+
|
| 1884 |
+
async def _exec(self, args, timeout=900):
|
| 1885 |
+
proc = await asyncio.create_subprocess_exec(
|
| 1886 |
+
*args, env=self._env(),
|
| 1887 |
+
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
| 1888 |
+
)
|
| 1889 |
+
try:
|
| 1890 |
+
out, err = await asyncio.wait_for(proc.communicate(), timeout)
|
| 1891 |
+
except asyncio.TimeoutError:
|
| 1892 |
+
proc.kill()
|
| 1893 |
+
await proc.wait()
|
| 1894 |
+
return 124, "", f"逾時 {timeout}s"
|
| 1895 |
+
return proc.returncode, out.decode(errors="replace"), err.decode(errors="replace")
|
| 1896 |
+
|
| 1897 |
+
def _probe_path(self):
|
| 1898 |
+
code = (
|
| 1899 |
+
"from playwright.sync_api import sync_playwright\n"
|
| 1900 |
+
"with sync_playwright() as p:\n"
|
| 1901 |
+
" print(p.chromium.executable_path)\n"
|
| 1902 |
+
)
|
| 1903 |
+
try:
|
| 1904 |
+
proc = subprocess.run(
|
| 1905 |
+
[sys.executable, "-c", code], capture_output=True, text=True,
|
| 1906 |
+
timeout=120, env=self._env(),
|
| 1907 |
+
)
|
| 1908 |
+
except Exception as exc:
|
| 1909 |
+
self.last_error = f"無法取得 Chromium 路徑:{exc}"
|
| 1910 |
+
return None
|
| 1911 |
+
lines = [ln.strip() for ln in (proc.stdout or "").splitlines() if ln.strip()]
|
| 1912 |
+
path = lines[-1] if lines else ""
|
| 1913 |
+
if proc.returncode != 0 or not path or not os.path.isfile(path):
|
| 1914 |
+
return None
|
| 1915 |
+
return path
|
| 1916 |
+
|
| 1917 |
+
def _launch_check(self):
|
| 1918 |
+
code = (
|
| 1919 |
+
"from playwright.sync_api import sync_playwright\n"
|
| 1920 |
+
"with sync_playwright() as p:\n"
|
| 1921 |
+
" b = p.chromium.launch(headless=True, args=['--no-sandbox','--disable-dev-shm-usage'])\n"
|
| 1922 |
+
" pg = b.new_page()\n"
|
| 1923 |
+
" pg.set_content('<title>webterm-playwright-ok</title>')\n"
|
| 1924 |
+
" print(pg.title())\n"
|
| 1925 |
+
" b.close()\n"
|
| 1926 |
+
)
|
| 1927 |
+
try:
|
| 1928 |
+
proc = subprocess.run(
|
| 1929 |
+
[sys.executable, "-c", code], capture_output=True, text=True,
|
| 1930 |
+
timeout=180, env=self._env(),
|
| 1931 |
+
)
|
| 1932 |
+
except Exception as exc:
|
| 1933 |
+
self.last_error = f"Chromium 啟動測試例外:{exc}"
|
| 1934 |
+
return False
|
| 1935 |
+
if proc.returncode != 0 or "webterm-playwright-ok" not in (proc.stdout or ""):
|
| 1936 |
+
detail = ((proc.stderr or "") or (proc.stdout or "")).strip()
|
| 1937 |
+
self.last_error = f"Chromium 啟動測試失敗:{detail[-500:]}"
|
| 1938 |
+
return False
|
| 1939 |
+
return True
|
| 1940 |
+
|
| 1941 |
+
def _write_launchers(self):
|
| 1942 |
+
icon = ""
|
| 1943 |
+
for name in ("product_logo_48.png", "product_logo_32.png"):
|
| 1944 |
+
candidate = os.path.join(os.path.dirname(self.chromium_path), name)
|
| 1945 |
+
if os.path.isfile(candidate):
|
| 1946 |
+
icon = candidate
|
| 1947 |
+
break
|
| 1948 |
+
script = (
|
| 1949 |
+
"#!/bin/bash\n"
|
| 1950 |
+
f"export DISPLAY=\"${{DISPLAY:-{DESKTOP_DISPLAY}}}\"\n"
|
| 1951 |
+
f"exec {shlex.quote(self.chromium_path)} --no-sandbox --disable-dev-shm-usage "
|
| 1952 |
+
"--no-first-run --no-default-browser-check --start-maximized \"$@\"\n"
|
| 1953 |
+
)
|
| 1954 |
+
entry = (
|
| 1955 |
+
"[Desktop Entry]\n"
|
| 1956 |
+
"Type=Application\n"
|
| 1957 |
+
"Version=1.0\n"
|
| 1958 |
+
f"Name={PW_TITLE}\n"
|
| 1959 |
+
"Comment=Chromium (Playwright) on the shared LXDE desktop\n"
|
| 1960 |
+
f"Exec={PW_LAUNCHER} %U\n"
|
| 1961 |
+
f"Icon={icon or 'web-browser'}\n"
|
| 1962 |
+
"Terminal=false\n"
|
| 1963 |
+
"Categories=Network;WebBrowser;\n"
|
| 1964 |
+
"StartupNotify=true\n"
|
| 1965 |
+
)
|
| 1966 |
+
os.makedirs(os.path.dirname(PW_LAUNCHER), exist_ok=True)
|
| 1967 |
+
with open(PW_LAUNCHER, "w") as f:
|
| 1968 |
+
f.write(script)
|
| 1969 |
+
os.chmod(PW_LAUNCHER, 0o755)
|
| 1970 |
+
for path in (PW_APP_ENTRY, PW_DESKTOP_ENTRY):
|
| 1971 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 1972 |
+
with open(path, "w") as f:
|
| 1973 |
+
f.write(entry)
|
| 1974 |
+
os.chmod(path, 0o755)
|
| 1975 |
+
|
| 1976 |
+
# ── 安裝 ──────────────────────────────────────────────────
|
| 1977 |
+
async def _install(self) -> bool:
|
| 1978 |
+
self.ready = False
|
| 1979 |
+
self.last_error = ""
|
| 1980 |
+
self._log("開始安裝 Playwright Chromium…")
|
| 1981 |
+
rc, _, _ = await self._exec([sys.executable, "-c", "import playwright"], timeout=60)
|
| 1982 |
+
if rc != 0:
|
| 1983 |
+
self._log("Python 套件 playwright 不存在,先以 pip 安裝", logging.WARNING)
|
| 1984 |
+
rc, out, err = await self._exec(
|
| 1985 |
+
[sys.executable, "-m", "pip", "install", "--no-cache-dir", "playwright"],
|
| 1986 |
+
timeout=600,
|
| 1987 |
+
)
|
| 1988 |
+
if rc != 0:
|
| 1989 |
+
self.last_error = f"pip install playwright 失敗:{(err or out)[-500:]}"
|
| 1990 |
+
self._log(self.last_error, logging.WARNING)
|
| 1991 |
+
return False
|
| 1992 |
+
|
| 1993 |
+
rc, out, err = await self._exec(
|
| 1994 |
+
[sys.executable, "-m", "playwright", "install", "chromium"], timeout=1800)
|
| 1995 |
+
if rc != 0:
|
| 1996 |
+
self._log("Chromium 下載失敗,先安裝系統依賴再重試", logging.WARNING)
|
| 1997 |
+
await self._exec(
|
| 1998 |
+
[sys.executable, "-m", "playwright", "install-deps", "chromium"], timeout=1800)
|
| 1999 |
+
rc, out, err = await self._exec(
|
| 2000 |
+
[sys.executable, "-m", "playwright", "install", "chromium"], timeout=1800)
|
| 2001 |
+
if rc != 0:
|
| 2002 |
+
self.last_error = f"playwright install chromium 失敗:{(err or out)[-500:]}"
|
| 2003 |
+
self._log(self.last_error, logging.WARNING)
|
| 2004 |
+
return False
|
| 2005 |
+
|
| 2006 |
+
path = await asyncio.to_thread(self._probe_path)
|
| 2007 |
+
if not path:
|
| 2008 |
+
self.last_error = self.last_error or "找不到 Playwright 的 Chromium 執行檔"
|
| 2009 |
+
self._log(self.last_error, logging.WARNING)
|
| 2010 |
+
return False
|
| 2011 |
+
self.chromium_path = path
|
| 2012 |
+
self.version = os.path.basename(os.path.dirname(os.path.dirname(path)))
|
| 2013 |
+
self._log(f"Chromium 已下載:{path}")
|
| 2014 |
+
|
| 2015 |
+
if not await asyncio.to_thread(self._launch_check):
|
| 2016 |
+
self._log(self.last_error + ";嘗試安裝系統依賴", logging.WARNING)
|
| 2017 |
+
await self._exec(
|
| 2018 |
+
[sys.executable, "-m", "playwright", "install-deps", "chromium"], timeout=1800)
|
| 2019 |
+
if not await asyncio.to_thread(self._launch_check):
|
| 2020 |
+
self._log(self.last_error, logging.WARNING)
|
| 2021 |
+
return False
|
| 2022 |
+
|
| 2023 |
+
try:
|
| 2024 |
+
await asyncio.to_thread(self._write_launchers)
|
| 2025 |
+
except Exception as exc:
|
| 2026 |
+
self.last_error = f"建立桌面啟動圖示失敗:{exc!r}"
|
| 2027 |
+
self._log(self.last_error, logging.WARNING)
|
| 2028 |
+
return False
|
| 2029 |
+
|
| 2030 |
+
self.ready = True
|
| 2031 |
+
self.installed_at = time.time()
|
| 2032 |
+
self.last_error = ""
|
| 2033 |
+
self._log(f"Playwright Chromium 就緒({self.version}),桌面圖示:{PW_DESKTOP_ENTRY}")
|
| 2034 |
+
try:
|
| 2035 |
+
pcman = shutil.which("pcmanfm")
|
| 2036 |
+
if pcman and _desktop_port_open():
|
| 2037 |
+
subprocess.Popen(
|
| 2038 |
+
[pcman, "--reconfigure"],
|
| 2039 |
+
env={**os.environ, "DISPLAY": DESKTOP_DISPLAY},
|
| 2040 |
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
| 2041 |
+
)
|
| 2042 |
+
except Exception:
|
| 2043 |
+
pass
|
| 2044 |
+
return True
|
| 2045 |
+
|
| 2046 |
+
async def ensure(self, force: bool = False) -> bool:
|
| 2047 |
+
async with self._lock:
|
| 2048 |
+
if not force and self.chromium_path and os.path.isfile(self.chromium_path):
|
| 2049 |
+
self.ready = True
|
| 2050 |
+
if not os.path.isfile(PW_LAUNCHER):
|
| 2051 |
+
try:
|
| 2052 |
+
await asyncio.to_thread(self._write_launchers)
|
| 2053 |
+
except Exception:
|
| 2054 |
+
pass
|
| 2055 |
+
return True
|
| 2056 |
+
now = time.time()
|
| 2057 |
+
if not force and now - self._last_attempt < 300:
|
| 2058 |
+
return False
|
| 2059 |
+
self._last_attempt = now
|
| 2060 |
+
self.installing = True
|
| 2061 |
+
try:
|
| 2062 |
+
return await self._install()
|
| 2063 |
+
except Exception as exc:
|
| 2064 |
+
self.last_error = f"安裝例外:{exc!r}"
|
| 2065 |
+
self._log(self.last_error, logging.WARNING)
|
| 2066 |
+
return False
|
| 2067 |
+
finally:
|
| 2068 |
+
self.installing = False
|
| 2069 |
+
|
| 2070 |
+
def status(self) -> dict:
|
| 2071 |
+
return {
|
| 2072 |
+
"ready": self.ready,
|
| 2073 |
+
"installing": self.installing,
|
| 2074 |
+
"chromium": self.chromium_path,
|
| 2075 |
+
"version": self.version,
|
| 2076 |
+
"browsers_path": PW_BROWSERS,
|
| 2077 |
+
"launcher": PW_LAUNCHER if os.path.isfile(PW_LAUNCHER) else None,
|
| 2078 |
+
"desktop_entry": PW_DESKTOP_ENTRY if os.path.isfile(PW_DESKTOP_ENTRY) else None,
|
| 2079 |
+
"installed_at": round(self.installed_at, 3) if self.installed_at else None,
|
| 2080 |
+
"last_error": self.last_error,
|
| 2081 |
+
}
|
| 2082 |
+
|
| 2083 |
+
|
| 2084 |
+
playwright_manager = PlaywrightManager()
|
| 2085 |
+
|
| 2086 |
+
|
| 2087 |
+
@app.get("/api/playwright/status")
|
| 2088 |
+
async def api_playwright_status():
|
| 2089 |
+
return playwright_manager.status()
|
| 2090 |
+
|
| 2091 |
+
|
| 2092 |
+
@app.post("/api/playwright/install")
|
| 2093 |
+
async def api_playwright_install():
|
| 2094 |
+
global PW_TASK
|
| 2095 |
+
if PW_TASK is None or PW_TASK.done():
|
| 2096 |
+
PW_TASK = asyncio.create_task(playwright_manager.ensure(force=True))
|
| 2097 |
+
return {"ok": True, "started": True, **playwright_manager.status()}
|
| 2098 |
+
|
| 2099 |
+
|
| 2100 |
+
async def playwright_watchdog():
|
| 2101 |
+
"""背景安裝 Chromium;完成後每 5 分鐘確認執行檔還在。"""
|
| 2102 |
+
await asyncio.sleep(10)
|
| 2103 |
+
while True:
|
| 2104 |
+
ok = False
|
| 2105 |
+
try:
|
| 2106 |
+
ok = await playwright_manager.ensure()
|
| 2107 |
+
except Exception as exc:
|
| 2108 |
+
log.warning("[playwright] 背景安裝例外(終端機不受影響):%s", exc)
|
| 2109 |
+
await asyncio.sleep(300 if ok else 60)
|
| 2110 |
+
|
| 2111 |
+
|
| 2112 |
async def desktop_watchdog():
|
| 2113 |
"""每 3 秒確認桌面還在,且永遠只有一份;不影響終端機流程。"""
|
| 2114 |
await asyncio.sleep(1)
|
|
|
|
| 2129 |
|
| 2130 |
@app.on_event("startup")
|
| 2131 |
async def startup_event():
|
| 2132 |
+
global DESKTOP_WATCHDOG, PW_WATCHDOG
|
| 2133 |
_load_tabs()
|
| 2134 |
_write_shell_launcher()
|
| 2135 |
await ensure_tmux()
|
|
|
|
| 2140 |
DESKTOP_WATCHDOG = asyncio.create_task(desktop_watchdog())
|
| 2141 |
except Exception as _desktop_err: # pragma: no cover
|
| 2142 |
log.warning("[desktop] 無法啟動看門狗(終端機照常運作):%r", _desktop_err)
|
| 2143 |
+
# Playwright Chromium 背景下載;完成後桌面會出現啟動圖示。
|
| 2144 |
+
try:
|
| 2145 |
+
if PW_WATCHDOG is None or PW_WATCHDOG.done():
|
| 2146 |
+
PW_WATCHDOG = asyncio.create_task(playwright_watchdog())
|
| 2147 |
+
except Exception as _pw_err: # pragma: no cover
|
| 2148 |
+
log.warning("[playwright] 無法啟動背景安裝(終端機照常運作):%r", _pw_err)
|
| 2149 |
dummy_gpu()
|
| 2150 |
if SKIP_INSTALL:
|
| 2151 |
log.info("WEBTERM_SKIP_INSTALL=1:跳過 opencode / 編輯器安裝")
|
packages.txt
CHANGED
|
@@ -5,3 +5,34 @@ lxde-core
|
|
| 5 |
xterm
|
| 6 |
novnc
|
| 7 |
x11-utils
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
xterm
|
| 6 |
novnc
|
| 7 |
x11-utils
|
| 8 |
+
libasound2t64
|
| 9 |
+
libatk-bridge2.0-0t64
|
| 10 |
+
libatk1.0-0t64
|
| 11 |
+
libatspi2.0-0t64
|
| 12 |
+
libcairo2
|
| 13 |
+
libcups2t64
|
| 14 |
+
libdbus-1-3
|
| 15 |
+
libdrm2
|
| 16 |
+
libgbm1
|
| 17 |
+
libglib2.0-0t64
|
| 18 |
+
libnspr4
|
| 19 |
+
libnss3
|
| 20 |
+
libpango-1.0-0
|
| 21 |
+
libx11-6
|
| 22 |
+
libxcb1
|
| 23 |
+
libxcomposite1
|
| 24 |
+
libxdamage1
|
| 25 |
+
libxext6
|
| 26 |
+
libxfixes3
|
| 27 |
+
libxkbcommon0
|
| 28 |
+
libxrandr2
|
| 29 |
+
fonts-noto-color-emoji
|
| 30 |
+
fonts-unifont
|
| 31 |
+
libfontconfig1
|
| 32 |
+
libfreetype6
|
| 33 |
+
xfonts-scalable
|
| 34 |
+
fonts-liberation
|
| 35 |
+
fonts-ipafont-gothic
|
| 36 |
+
fonts-wqy-zenhei
|
| 37 |
+
fonts-tlwg-loma-otf
|
| 38 |
+
fonts-freefont-ttf
|
requirements.txt
CHANGED
|
@@ -2,3 +2,4 @@ fastapi
|
|
| 2 |
uvicorn[standard]
|
| 3 |
websockets
|
| 4 |
httpx
|
|
|
|
|
|
| 2 |
uvicorn[standard]
|
| 3 |
websockets
|
| 4 |
httpx
|
| 5 |
+
playwright
|