mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-21 06:29:24 +08:00
* feat: add remote desktop streaming for Linux, Windows, and macOS Introduce dashboard remote desktop with WebSocket JPEG streaming, host input injection, and admin control panel shortcuts. Add backend desktop domain modules, Linux install scripts, native capture on Win/Mac, session supersede, and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: stop misleading Mac remote-desktop setup prompts Only surface Linux install/start commands on Linux, probe macOS TCC for missing permissions, and hide setup banners once streaming. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: make desktop setup mypy-clean on Windows and Linux CI Use posix_compat.geteuid instead of os.geteuid, and drop unused type: ignore on optional macOS TCC imports. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: make desktop capture/input unit tests CI-safe Force linux platform for virtual-display paths and avoid requiring Pillow/pynput extras in the default test environment. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: polish remote desktop install/uninstall UX and bundle Linux scripts Ship virtual-desktop scripts in the package, add localized SSE install/uninstall flows with viewport progress, and fix uninstall success detection on hosts that keep a usable display after the virtual stack is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden desktop install SSE streaming and clear dashboard lint Read apt/subprocess output in chunks to avoid asyncio readline crashes, surface friendly apt-lock errors, and fix three dashboard eslint issues. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: jubaoliang <jubaoliang@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
3.5 KiB
Python
113 lines
3.5 KiB
Python
"""Tests for desktop screen capture helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import sys
|
|
import types
|
|
from unittest.mock import MagicMock
|
|
|
|
from octop.infra.desktop.capture import CaptureFrame, ScreenCapture
|
|
|
|
|
|
def _force_linux(monkeypatch) -> None:
|
|
monkeypatch.setattr("octop.infra.desktop.capture.sys.platform", "linux")
|
|
|
|
|
|
def test_prefer_import_on_linux_virtual_display(monkeypatch) -> None:
|
|
_force_linux(monkeypatch)
|
|
cap = ScreenCapture(display=":99")
|
|
monkeypatch.setattr(
|
|
"octop.infra.desktop.capture.shutil.which",
|
|
lambda name: "/usr/bin/import" if name == "import" else None,
|
|
)
|
|
assert cap._prefer_import_capture() is True
|
|
|
|
|
|
def test_prefer_import_false_on_non_linux(monkeypatch) -> None:
|
|
cap = ScreenCapture(display=":99")
|
|
monkeypatch.setattr("octop.infra.desktop.capture.sys.platform", "darwin")
|
|
assert cap._prefer_import_capture() is False
|
|
|
|
|
|
def test_capture_prefers_import_on_linux_virtual_display(monkeypatch) -> None:
|
|
_force_linux(monkeypatch)
|
|
monkeypatch.setattr(
|
|
"octop.infra.desktop.capture.shutil.which",
|
|
lambda name: "/usr/bin/import" if name == "import" else None,
|
|
)
|
|
frame = CaptureFrame(jpeg_b64="abc", width=100, height=80)
|
|
cap = ScreenCapture(display=":99", monitor=0)
|
|
mss_calls: list[int] = []
|
|
|
|
monkeypatch.setattr(
|
|
cap,
|
|
"_capture_imagemagick",
|
|
lambda *, quality: frame,
|
|
)
|
|
monkeypatch.setattr(
|
|
cap,
|
|
"_capture_mss",
|
|
lambda *, quality: mss_calls.append(quality) or None,
|
|
)
|
|
got = cap.capture_jpeg(quality=70)
|
|
assert got == frame
|
|
assert mss_calls == []
|
|
|
|
|
|
def test_capture_prefers_mss_on_non_virtual_display(monkeypatch) -> None:
|
|
_force_linux(monkeypatch)
|
|
frame = CaptureFrame(jpeg_b64="abc", width=100, height=80)
|
|
cap = ScreenCapture(display=":0", monitor=0)
|
|
import_calls: list[int] = []
|
|
|
|
monkeypatch.setattr(cap, "_capture_mss", lambda *, quality: frame)
|
|
monkeypatch.setattr(
|
|
cap,
|
|
"_capture_imagemagick",
|
|
lambda *, quality: import_calls.append(quality) or None,
|
|
)
|
|
got = cap.capture_jpeg(quality=70)
|
|
assert got == frame
|
|
assert import_calls == []
|
|
|
|
|
|
def test_import_jpeg_passthrough_without_reencode(monkeypatch) -> None:
|
|
# Avoid requiring octop[desktop] / Pillow in default CI installs.
|
|
raw = b"\xff\xd8" + b"\x00" * 32 + b"\xff\xd9"
|
|
fake_img = MagicMock()
|
|
fake_img.size = (64, 48)
|
|
|
|
pil = types.ModuleType("PIL")
|
|
pil_image = types.ModuleType("PIL.Image")
|
|
pil_image.open = lambda *_a, **_k: fake_img # type: ignore[attr-defined]
|
|
pil.Image = pil_image # type: ignore[attr-defined]
|
|
monkeypatch.setitem(sys.modules, "PIL", pil)
|
|
monkeypatch.setitem(sys.modules, "PIL.Image", pil_image)
|
|
|
|
cap = ScreenCapture(display=":99", monitor=0)
|
|
|
|
class _Proc:
|
|
returncode = 0
|
|
|
|
def communicate(self, timeout: float | None = None) -> tuple[bytes, bytes]:
|
|
return raw, b""
|
|
|
|
monkeypatch.setattr("octop.infra.desktop.capture.shutil.which", lambda name: "/usr/bin/import")
|
|
monkeypatch.setattr("octop.infra.desktop.capture.subprocess.Popen", lambda *a, **k: _Proc())
|
|
|
|
frame = cap._capture_imagemagick(quality=70)
|
|
assert frame is not None
|
|
assert frame.width == 64
|
|
assert frame.height == 48
|
|
assert frame.jpeg_b64 == base64.b64encode(raw).decode("ascii")
|
|
|
|
|
|
def test_close_releases_mss() -> None:
|
|
cap = ScreenCapture(display=":99", monitor=0)
|
|
mock_mss = MagicMock()
|
|
cap._mss = mock_mss
|
|
cap.close()
|
|
mock_mss.close.assert_called_once()
|
|
assert cap._mss is None
|