vdisplay 0.1.5__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.
- vdisplay/__init__.py +12 -0
- vdisplay/agent_config.py +71 -0
- vdisplay/agent_dispatch.py +30 -0
- vdisplay/agent_envelope.py +17 -0
- vdisplay/api.py +193 -0
- vdisplay/application/__init__.py +14 -0
- vdisplay/application/commands.py +183 -0
- vdisplay/application/errors.py +39 -0
- vdisplay/application/executor.py +51 -0
- vdisplay/application/handlers/__init__.py +6 -0
- vdisplay/application/handlers/agent.py +191 -0
- vdisplay/application/handlers/local.py +173 -0
- vdisplay/application/runtime.py +82 -0
- vdisplay/application/services/__init__.py +3 -0
- vdisplay/application/services/capture.py +182 -0
- vdisplay/application/services/discovery.py +236 -0
- vdisplay/application/services/img2nl_enrich.py +97 -0
- vdisplay/application/services/info.py +51 -0
- vdisplay/application/services/sampler.py +136 -0
- vdisplay/application/services/session.py +198 -0
- vdisplay/backends/__init__.py +1 -0
- vdisplay/backends/base.py +64 -0
- vdisplay/backends/linux_x11_mirror.py +259 -0
- vdisplay/backends/linux_x11_relay.py +478 -0
- vdisplay/backends/linux_xvfb.py +164 -0
- vdisplay/backends/mirror_stub.py +34 -0
- vdisplay/capture/__init__.py +15 -0
- vdisplay/capture/base.py +9 -0
- vdisplay/capture/host.py +484 -0
- vdisplay/capture/linux_xwd.py +320 -0
- vdisplay/capture/policy.py +116 -0
- vdisplay/capture/portal.py +221 -0
- vdisplay/capture/portal_screencast.py +690 -0
- vdisplay/capture/providers/__init__.py +3 -0
- vdisplay/capture/providers/base.py +22 -0
- vdisplay/capture/providers/drm.py +92 -0
- vdisplay/capture/providers/engine.py +99 -0
- vdisplay/capture/providers/fbdev.py +77 -0
- vdisplay/capture/providers/mss.py +60 -0
- vdisplay/capture/providers/x11.py +35 -0
- vdisplay/cli.py +32 -0
- vdisplay/cli_handlers.py +34 -0
- vdisplay/client.py +263 -0
- vdisplay/commands/__init__.py +42 -0
- vdisplay/commands/agent.py +104 -0
- vdisplay/commands/all_cmd.py +46 -0
- vdisplay/commands/common.py +35 -0
- vdisplay/commands/diagnose.py +26 -0
- vdisplay/commands/info.py +16 -0
- vdisplay/commands/io.py +7 -0
- vdisplay/commands/mirror.py +53 -0
- vdisplay/commands/monitors.py +19 -0
- vdisplay/commands/nlp.py +23 -0
- vdisplay/commands/relay.py +97 -0
- vdisplay/commands/sampler.py +68 -0
- vdisplay/commands/screenshot.py +53 -0
- vdisplay/commands/virtual.py +81 -0
- vdisplay/commands/windows.py +29 -0
- vdisplay/discovery.py +352 -0
- vdisplay/exceptions.py +10 -0
- vdisplay/input/__init__.py +3 -0
- vdisplay/input/linux_xdotool.py +45 -0
- vdisplay/models.py +26 -0
- vdisplay/nl.py +158 -0
- vdisplay/nlp.py +158 -0
- vdisplay/payloads.py +86 -0
- vdisplay/utils.py +46 -0
- vdisplay/windows/__init__.py +46 -0
- vdisplay/windows/constants.py +19 -0
- vdisplay/windows/filter.py +173 -0
- vdisplay/windows/normalize.py +103 -0
- vdisplay/windows/query.py +209 -0
- vdisplay/windows/rank.py +43 -0
- vdisplay/windows/scan.py +110 -0
- vdisplay-0.1.5.dist-info/METADATA +612 -0
- vdisplay-0.1.5.dist-info/RECORD +80 -0
- vdisplay-0.1.5.dist-info/WHEEL +5 -0
- vdisplay-0.1.5.dist-info/entry_points.txt +2 -0
- vdisplay-0.1.5.dist-info/licenses/LICENSE +201 -0
- vdisplay-0.1.5.dist-info/top_level.txt +1 -0
vdisplay/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .api import MirrorSession, VirtualDisplaySession, WindowRelaySession, platform_summary
|
|
2
|
+
from .exceptions import BackendNotAvailableError, CapabilityError, VDisplayError
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"VirtualDisplaySession",
|
|
6
|
+
"MirrorSession",
|
|
7
|
+
"WindowRelaySession",
|
|
8
|
+
"platform_summary",
|
|
9
|
+
"VDisplayError",
|
|
10
|
+
"BackendNotAvailableError",
|
|
11
|
+
"CapabilityError",
|
|
12
|
+
]
|
vdisplay/agent_config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Resolve vdisplay-agent connection (install-once broker for all client apps)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
_PROBE_SENTINEL = object()
|
|
10
|
+
_probe_cache: str | None | object = _PROBE_SENTINEL
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def agent_auto_enabled() -> bool:
|
|
14
|
+
return os.environ.get("VDISPLAY_AGENT_AUTO", "1").strip().lower() not in {
|
|
15
|
+
"0",
|
|
16
|
+
"false",
|
|
17
|
+
"no",
|
|
18
|
+
"off",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def reset_agent_probe_cache() -> None:
|
|
23
|
+
global _probe_cache
|
|
24
|
+
_probe_cache = _PROBE_SENTINEL
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _default_agent_base() -> str:
|
|
28
|
+
host = os.environ.get("VDISPLAY_AGENT_HOST", "127.0.0.1").strip() or "127.0.0.1"
|
|
29
|
+
port = os.environ.get("VDISPLAY_AGENT_PORT", "8765").strip() or "8765"
|
|
30
|
+
return f"http://{host}:{port}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _probe_agent_url(base_url: str, *, timeout: float = 0.2) -> str | None:
|
|
34
|
+
try:
|
|
35
|
+
with urllib.request.urlopen(f"{base_url.rstrip('/')}/health", timeout=timeout) as response:
|
|
36
|
+
if response.status == 200:
|
|
37
|
+
return base_url.rstrip("/")
|
|
38
|
+
except (urllib.error.URLError, TimeoutError, OSError, ValueError):
|
|
39
|
+
return None
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _probe_default_agent() -> str | None:
|
|
44
|
+
global _probe_cache
|
|
45
|
+
if isinstance(_probe_cache, str):
|
|
46
|
+
return _probe_cache
|
|
47
|
+
url = _probe_agent_url(_default_agent_base())
|
|
48
|
+
if url:
|
|
49
|
+
_probe_cache = url
|
|
50
|
+
return url
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolve_agent_url(explicit: str | None = None, *, allow_auto: bool = False) -> str | None:
|
|
54
|
+
"""Return agent base URL when clients should use IPC instead of in-process capture."""
|
|
55
|
+
url = (explicit or os.environ.get("VDISPLAY_AGENT_URL") or "").strip()
|
|
56
|
+
if url:
|
|
57
|
+
return url.rstrip("/")
|
|
58
|
+
if not allow_auto or not agent_auto_enabled():
|
|
59
|
+
return None
|
|
60
|
+
return _probe_default_agent()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def resolve_agent_token() -> str | None:
|
|
64
|
+
token = (os.environ.get("VDISPLAY_AGENT_TOKEN") or "").strip()
|
|
65
|
+
return token or None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def use_agent(explicit: str | None = None) -> bool:
|
|
69
|
+
if os.environ.get("VDISPLAY_AGENT_BROKER", "").strip().lower() in {"1", "true", "yes"}:
|
|
70
|
+
return False
|
|
71
|
+
return resolve_agent_url(explicit, allow_auto=True) is not None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Deprecated — route DSL verbs through application.executor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .agent_config import resolve_agent_url
|
|
9
|
+
from .application.commands import CommandRequest
|
|
10
|
+
from .application.executor import execute
|
|
11
|
+
from .client import AgentClient
|
|
12
|
+
from .exceptions import VDisplayError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def agent_client(url: str | None = None) -> AgentClient:
|
|
16
|
+
resolved = resolve_agent_url(url)
|
|
17
|
+
if not resolved:
|
|
18
|
+
raise VDisplayError("VDISPLAY_AGENT_URL is not set")
|
|
19
|
+
return AgentClient(resolved)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def dispatch_via_agent(cmd: dict[str, Any], *, line: str) -> Any:
|
|
23
|
+
"""Execute parsed DSL command through vdisplay-agent (via executor)."""
|
|
24
|
+
warnings.warn(
|
|
25
|
+
"agent_dispatch.dispatch_via_agent is deprecated; use application.executor.execute",
|
|
26
|
+
DeprecationWarning,
|
|
27
|
+
stacklevel=2,
|
|
28
|
+
)
|
|
29
|
+
request = CommandRequest.from_dsl(cmd, line=line)
|
|
30
|
+
return execute(request, force_route="agent").to_dsl_result()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Flatten vdisplay-agent JSON envelopes for SDK clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def flatten_agent_envelope(payload: dict[str, Any]) -> dict[str, Any]:
|
|
9
|
+
"""Merge envelope.data to top level for backward-compatible clients."""
|
|
10
|
+
if "data" in payload and "action" in payload:
|
|
11
|
+
flat = dict(payload.get("data") or {})
|
|
12
|
+
if "ok" in payload:
|
|
13
|
+
flat["ok"] = payload["ok"]
|
|
14
|
+
if payload.get("error"):
|
|
15
|
+
flat["error"] = payload["error"]
|
|
16
|
+
return flat
|
|
17
|
+
return payload
|
vdisplay/api.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
from .backends.linux_x11_mirror import LinuxX11MirrorBackend
|
|
8
|
+
from .backends.linux_x11_relay import LinuxX11RelayBackend
|
|
9
|
+
from .backends.linux_xvfb import LinuxXvfbBackend
|
|
10
|
+
from .exceptions import BackendNotAvailableError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _default_virtual_backend() -> str:
|
|
14
|
+
if sys.platform.startswith("linux"):
|
|
15
|
+
return "xvfb"
|
|
16
|
+
return "xvfb"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_mirror_backend() -> str:
|
|
20
|
+
if sys.platform.startswith("linux"):
|
|
21
|
+
return "x11"
|
|
22
|
+
return "stub"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _default_relay_backend() -> str:
|
|
26
|
+
if sys.platform.startswith("linux"):
|
|
27
|
+
return "x11"
|
|
28
|
+
return "x11"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VirtualDisplaySession:
|
|
32
|
+
def __init__(self, backend) -> None:
|
|
33
|
+
self.backend = backend
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def create(
|
|
37
|
+
cls,
|
|
38
|
+
width: int = 1920,
|
|
39
|
+
height: int = 1080,
|
|
40
|
+
backend: str | None = None,
|
|
41
|
+
display: str = ":99",
|
|
42
|
+
):
|
|
43
|
+
backend = backend or _default_virtual_backend()
|
|
44
|
+
if backend == "xvfb":
|
|
45
|
+
if not sys.platform.startswith("linux"):
|
|
46
|
+
raise BackendNotAvailableError("xvfb backend is only available on Linux")
|
|
47
|
+
return cls(LinuxXvfbBackend(width=width, height=height, display=display))
|
|
48
|
+
raise BackendNotAvailableError(f"Unknown virtual backend: {backend}")
|
|
49
|
+
|
|
50
|
+
def start(self) -> None:
|
|
51
|
+
self.backend.start()
|
|
52
|
+
|
|
53
|
+
def stop(self) -> None:
|
|
54
|
+
self.backend.stop()
|
|
55
|
+
|
|
56
|
+
def launch(self, command):
|
|
57
|
+
return self.backend.launch(command)
|
|
58
|
+
|
|
59
|
+
def screenshot_bytes(self) -> bytes:
|
|
60
|
+
return self.backend.screenshot_bytes()
|
|
61
|
+
|
|
62
|
+
def save_screenshot(self, path: str) -> str:
|
|
63
|
+
return self.backend.save_screenshot(path)
|
|
64
|
+
|
|
65
|
+
def adopt_window(self, *, match_title: str | None = None, window_id: str | None = None) -> str:
|
|
66
|
+
return self.backend.adopt_window(match_title=match_title, window_id=window_id)
|
|
67
|
+
|
|
68
|
+
def release_window(self, *, match_title: str | None = None, window_id: str | None = None) -> str:
|
|
69
|
+
return self.backend.release_window(match_title=match_title, window_id=window_id)
|
|
70
|
+
|
|
71
|
+
def info(self) -> dict:
|
|
72
|
+
return asdict(self.backend.info())
|
|
73
|
+
|
|
74
|
+
def capabilities(self) -> dict:
|
|
75
|
+
return asdict(self.backend.capabilities())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MirrorSession:
|
|
79
|
+
def __init__(self, backend) -> None:
|
|
80
|
+
self.backend = backend
|
|
81
|
+
self.pointer = getattr(backend, "pointer", None)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def create(
|
|
85
|
+
cls,
|
|
86
|
+
source: str = "primary",
|
|
87
|
+
target: str | None = None,
|
|
88
|
+
backend: str | None = None,
|
|
89
|
+
display: str | None = None,
|
|
90
|
+
):
|
|
91
|
+
backend = backend or _default_mirror_backend()
|
|
92
|
+
if backend in {"x11", "linux-x11", "linux-x11-mirror"}:
|
|
93
|
+
if not sys.platform.startswith("linux"):
|
|
94
|
+
raise BackendNotAvailableError("linux-x11 mirror backend is only available on Linux")
|
|
95
|
+
return cls(LinuxX11MirrorBackend(source=source, target=target, display=display))
|
|
96
|
+
if backend == "stub":
|
|
97
|
+
from .backends.mirror_stub import MirrorStubBackend
|
|
98
|
+
|
|
99
|
+
return cls(MirrorStubBackend(source=source, target=target or "virtual:1"))
|
|
100
|
+
raise BackendNotAvailableError(f"Unknown mirror backend: {backend}")
|
|
101
|
+
|
|
102
|
+
def start(self) -> None:
|
|
103
|
+
self.backend.start()
|
|
104
|
+
|
|
105
|
+
def stop(self) -> None:
|
|
106
|
+
self.backend.stop()
|
|
107
|
+
|
|
108
|
+
def screenshot_bytes(self) -> bytes:
|
|
109
|
+
return self.backend.screenshot_bytes()
|
|
110
|
+
|
|
111
|
+
def save_screenshot(self, path: str) -> str:
|
|
112
|
+
return self.backend.save_screenshot(path)
|
|
113
|
+
|
|
114
|
+
def info(self) -> dict:
|
|
115
|
+
return asdict(self.backend.info())
|
|
116
|
+
|
|
117
|
+
def capabilities(self) -> dict:
|
|
118
|
+
return asdict(self.backend.capabilities())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class WindowRelaySession:
|
|
122
|
+
def __init__(self, backend) -> None:
|
|
123
|
+
self.backend = backend
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def create(cls, backend: str | None = None, display: str | None = None):
|
|
127
|
+
backend = backend or _default_relay_backend()
|
|
128
|
+
if backend in {"x11", "linux-x11", "linux-x11-relay"}:
|
|
129
|
+
if not sys.platform.startswith("linux"):
|
|
130
|
+
raise BackendNotAvailableError("linux-x11 relay backend is only available on Linux")
|
|
131
|
+
return cls(LinuxX11RelayBackend(display=display))
|
|
132
|
+
raise BackendNotAvailableError(f"Unknown relay backend: {backend}")
|
|
133
|
+
|
|
134
|
+
def start(self) -> None:
|
|
135
|
+
self.backend.start()
|
|
136
|
+
|
|
137
|
+
def stop(self) -> None:
|
|
138
|
+
self.backend.stop()
|
|
139
|
+
|
|
140
|
+
def adopt_window(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
match_title: str | None = None,
|
|
144
|
+
window_id: str | None = None,
|
|
145
|
+
match_class: str | None = None,
|
|
146
|
+
match_pid: int | None = None,
|
|
147
|
+
match_app: str | None = None,
|
|
148
|
+
target: str = "offscreen",
|
|
149
|
+
) -> str:
|
|
150
|
+
return self.backend.adopt_window(
|
|
151
|
+
match_title=match_title,
|
|
152
|
+
window_id=window_id,
|
|
153
|
+
match_class=match_class,
|
|
154
|
+
match_pid=match_pid,
|
|
155
|
+
match_app=match_app,
|
|
156
|
+
target=target,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def release_window(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
match_title: str | None = None,
|
|
163
|
+
window_id: str | None = None,
|
|
164
|
+
match_class: str | None = None,
|
|
165
|
+
match_pid: int | None = None,
|
|
166
|
+
match_app: str | None = None,
|
|
167
|
+
) -> str:
|
|
168
|
+
return self.backend.release_window(
|
|
169
|
+
match_title=match_title,
|
|
170
|
+
window_id=window_id,
|
|
171
|
+
match_class=match_class,
|
|
172
|
+
match_pid=match_pid,
|
|
173
|
+
match_app=match_app,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def list_adopted(self) -> list[dict]:
|
|
177
|
+
return self.backend.list_adopted()
|
|
178
|
+
|
|
179
|
+
def info(self) -> dict:
|
|
180
|
+
return asdict(self.backend.info())
|
|
181
|
+
|
|
182
|
+
def capabilities(self) -> dict:
|
|
183
|
+
return asdict(self.backend.capabilities())
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def platform_summary() -> dict:
|
|
187
|
+
return {
|
|
188
|
+
"platform": platform.system(),
|
|
189
|
+
"python": platform.python_version(),
|
|
190
|
+
"virtual_backend": _default_virtual_backend(),
|
|
191
|
+
"mirror_backend": _default_mirror_backend(),
|
|
192
|
+
"relay_backend": _default_relay_backend(),
|
|
193
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Application use-cases — single execution layer for all interfaces."""
|
|
2
|
+
|
|
3
|
+
from . import commands, errors
|
|
4
|
+
from .services import capture, discovery, info, session
|
|
5
|
+
|
|
6
|
+
__all__ = ["capture", "commands", "discovery", "errors", "executor", "info", "session"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def __getattr__(name: str):
|
|
10
|
+
if name == "executor":
|
|
11
|
+
from . import executor as executor_mod
|
|
12
|
+
|
|
13
|
+
return executor_mod
|
|
14
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Shared command model for CLI, DSL, REST, and agent client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import ApplicationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CommandVerb(StrEnum):
|
|
14
|
+
HEALTH = "HEALTH"
|
|
15
|
+
INFO = "INFO"
|
|
16
|
+
OUTPUTS = "OUTPUTS"
|
|
17
|
+
MONITORS = "MONITORS"
|
|
18
|
+
WINDOWS = "WINDOWS"
|
|
19
|
+
ALL = "ALL"
|
|
20
|
+
CAPABILITIES = "CAPABILITIES"
|
|
21
|
+
VALIDATE = "VALIDATE"
|
|
22
|
+
SCREENSHOT = "SCREENSHOT"
|
|
23
|
+
VIRTUAL_START = "VIRTUAL_START"
|
|
24
|
+
VIRTUAL_STOP = "VIRTUAL_STOP"
|
|
25
|
+
LAUNCH = "LAUNCH"
|
|
26
|
+
MIRROR = "MIRROR"
|
|
27
|
+
ADOPT = "ADOPT"
|
|
28
|
+
RELEASE = "RELEASE"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
QUERY_VERBS = frozenset(
|
|
32
|
+
{
|
|
33
|
+
CommandVerb.HEALTH,
|
|
34
|
+
CommandVerb.INFO,
|
|
35
|
+
CommandVerb.OUTPUTS,
|
|
36
|
+
CommandVerb.MONITORS,
|
|
37
|
+
CommandVerb.WINDOWS,
|
|
38
|
+
CommandVerb.ALL,
|
|
39
|
+
CommandVerb.CAPABILITIES,
|
|
40
|
+
CommandVerb.VALIDATE,
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
COMMAND_VERBS = frozenset(
|
|
45
|
+
{
|
|
46
|
+
CommandVerb.SCREENSHOT,
|
|
47
|
+
CommandVerb.VIRTUAL_START,
|
|
48
|
+
CommandVerb.VIRTUAL_STOP,
|
|
49
|
+
CommandVerb.LAUNCH,
|
|
50
|
+
CommandVerb.MIRROR,
|
|
51
|
+
CommandVerb.ADOPT,
|
|
52
|
+
CommandVerb.RELEASE,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class CommandRequest:
|
|
59
|
+
verb: CommandVerb
|
|
60
|
+
line: str = ""
|
|
61
|
+
display: str | None = None
|
|
62
|
+
apps_only: bool = False
|
|
63
|
+
include_all: bool = True
|
|
64
|
+
match_class: str | None = None
|
|
65
|
+
match_pid: int | None = None
|
|
66
|
+
match_app: str | None = None
|
|
67
|
+
match_title: str | None = None
|
|
68
|
+
window_id: str | None = None
|
|
69
|
+
min_width: int = 0
|
|
70
|
+
min_height: int = 0
|
|
71
|
+
output: str | None = None
|
|
72
|
+
width: int = 1920
|
|
73
|
+
height: int = 1080
|
|
74
|
+
source: str | None = None
|
|
75
|
+
target: str | None = None
|
|
76
|
+
mode: str = "host"
|
|
77
|
+
all_monitors: bool = False
|
|
78
|
+
out_dir: str | None = None
|
|
79
|
+
vd_display: str = ":99"
|
|
80
|
+
backend: str = "xvfb"
|
|
81
|
+
monitor: int | None = None
|
|
82
|
+
local_only: bool = False
|
|
83
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def action(self) -> str:
|
|
87
|
+
if self.verb == CommandVerb.OUTPUTS:
|
|
88
|
+
return "outputs"
|
|
89
|
+
return self.verb.value.lower()
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_dsl(cls, cmd: dict[str, Any], *, line: str = "") -> CommandRequest:
|
|
93
|
+
verb_raw = str(cmd.get("verb", "HEALTH")).upper()
|
|
94
|
+
try:
|
|
95
|
+
verb = CommandVerb(verb_raw)
|
|
96
|
+
except ValueError:
|
|
97
|
+
verb = CommandVerb.HEALTH
|
|
98
|
+
apps_only = bool(cmd.get("apps_only", False))
|
|
99
|
+
return cls(
|
|
100
|
+
verb=verb,
|
|
101
|
+
line=line,
|
|
102
|
+
display=cmd.get("display"),
|
|
103
|
+
apps_only=apps_only,
|
|
104
|
+
include_all=not apps_only,
|
|
105
|
+
match_class=cmd.get("class"),
|
|
106
|
+
match_pid=cmd.get("pid"),
|
|
107
|
+
match_app=cmd.get("app"),
|
|
108
|
+
match_title=cmd.get("title"),
|
|
109
|
+
window_id=cmd.get("window_id"),
|
|
110
|
+
output=cmd.get("out"),
|
|
111
|
+
width=int(cmd.get("width", 1920)),
|
|
112
|
+
height=int(cmd.get("height", 1080)),
|
|
113
|
+
source=cmd.get("source"),
|
|
114
|
+
target=cmd.get("target"),
|
|
115
|
+
vd_display=str(cmd.get("display", ":99")),
|
|
116
|
+
backend=str(cmd.get("backend", "xvfb")),
|
|
117
|
+
extra={k: v for k, v in cmd.items() if k not in {"verb"}},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class CommandResult:
|
|
123
|
+
ok: bool
|
|
124
|
+
action: str
|
|
125
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
126
|
+
error: ApplicationError | None = None
|
|
127
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
128
|
+
command: str = ""
|
|
129
|
+
|
|
130
|
+
def to_dict(self) -> dict[str, Any]:
|
|
131
|
+
payload: dict[str, Any] = {
|
|
132
|
+
"ok": self.ok,
|
|
133
|
+
"action": self.action,
|
|
134
|
+
"data": self.data,
|
|
135
|
+
"meta": self.meta,
|
|
136
|
+
}
|
|
137
|
+
if self.command:
|
|
138
|
+
payload["command"] = self.command
|
|
139
|
+
if self.error is not None:
|
|
140
|
+
payload["error"] = self.error.to_dict()
|
|
141
|
+
return payload
|
|
142
|
+
|
|
143
|
+
def to_dsl_result(self) -> Any:
|
|
144
|
+
from dsl2vdisplay.result import DslResult
|
|
145
|
+
|
|
146
|
+
return DslResult(
|
|
147
|
+
ok=self.ok,
|
|
148
|
+
command=self.command,
|
|
149
|
+
action=self.action,
|
|
150
|
+
output=json.dumps(self.data, indent=2, ensure_ascii=False) if self.ok else "",
|
|
151
|
+
data=self.data,
|
|
152
|
+
error=None if self.ok else (self.error.message if self.error else "unknown error"),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def success(
|
|
157
|
+
cls,
|
|
158
|
+
*,
|
|
159
|
+
action: str,
|
|
160
|
+
data: dict[str, Any],
|
|
161
|
+
command: str = "",
|
|
162
|
+
meta: dict[str, Any] | None = None,
|
|
163
|
+
) -> CommandResult:
|
|
164
|
+
return cls(ok=True, action=action, data=data, command=command, meta=meta or {})
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def failure(
|
|
168
|
+
cls,
|
|
169
|
+
*,
|
|
170
|
+
action: str,
|
|
171
|
+
error: ApplicationError,
|
|
172
|
+
data: dict[str, Any] | None = None,
|
|
173
|
+
command: str = "",
|
|
174
|
+
meta: dict[str, Any] | None = None,
|
|
175
|
+
) -> CommandResult:
|
|
176
|
+
return cls(
|
|
177
|
+
ok=False,
|
|
178
|
+
action=action,
|
|
179
|
+
data=data or {},
|
|
180
|
+
error=error,
|
|
181
|
+
command=command,
|
|
182
|
+
meta=meta or {},
|
|
183
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Stable error codes for command execution and agent HTTP envelope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ErrorCode(StrEnum):
|
|
11
|
+
NOT_SUPPORTED = "not_supported"
|
|
12
|
+
DEPENDENCY_MISSING = "dependency_missing"
|
|
13
|
+
PERMISSION_REQUIRED = "permission_required"
|
|
14
|
+
SESSION_NOT_FOUND = "session_not_found"
|
|
15
|
+
INVALID_REQUEST = "invalid_request"
|
|
16
|
+
BACKEND_UNAVAILABLE = "backend_unavailable"
|
|
17
|
+
INTERNAL = "internal"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ApplicationError:
|
|
22
|
+
code: ErrorCode
|
|
23
|
+
message: str
|
|
24
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
return {"code": self.code.value, "message": self.message, "details": self.details}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def error_from_exception(exc: Exception) -> ApplicationError:
|
|
31
|
+
from ..exceptions import BackendNotAvailableError, CapabilityError, VDisplayError
|
|
32
|
+
|
|
33
|
+
if isinstance(exc, CapabilityError):
|
|
34
|
+
return ApplicationError(ErrorCode.NOT_SUPPORTED, str(exc))
|
|
35
|
+
if isinstance(exc, BackendNotAvailableError):
|
|
36
|
+
return ApplicationError(ErrorCode.BACKEND_UNAVAILABLE, str(exc))
|
|
37
|
+
if isinstance(exc, VDisplayError):
|
|
38
|
+
return ApplicationError(ErrorCode.INVALID_REQUEST, str(exc))
|
|
39
|
+
return ApplicationError(ErrorCode.INTERNAL, str(exc))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Execute CommandRequest via agent or local application services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .commands import CommandRequest, CommandResult, CommandVerb
|
|
8
|
+
from .errors import error_from_exception
|
|
9
|
+
from .handlers import execute_agent, execute_local
|
|
10
|
+
from .runtime import ExecutionPolicy, Route, get_execution_policy
|
|
11
|
+
from ..exceptions import VDisplayError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _maybe_enrich_screenshot(cmd: CommandRequest, data: dict[str, Any]) -> dict[str, Any]:
|
|
15
|
+
if cmd.verb != CommandVerb.SCREENSHOT:
|
|
16
|
+
return data
|
|
17
|
+
if cmd.extra.get("skip_img2nl"):
|
|
18
|
+
return data
|
|
19
|
+
from .services import img2nl_enrich
|
|
20
|
+
|
|
21
|
+
return img2nl_enrich.enrich_screenshot_payload(data)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def execute(
|
|
25
|
+
cmd: CommandRequest,
|
|
26
|
+
*,
|
|
27
|
+
policy: ExecutionPolicy | None = None,
|
|
28
|
+
force_route: Route | None = None,
|
|
29
|
+
) -> CommandResult:
|
|
30
|
+
"""Single entry for command execution across CLI, DSL, REST, and agent dispatch."""
|
|
31
|
+
pol = policy or get_execution_policy()
|
|
32
|
+
route = force_route or pol.route(cmd)
|
|
33
|
+
meta = pol.meta_for(route)
|
|
34
|
+
try:
|
|
35
|
+
data = execute_agent(cmd) if route == "agent" else execute_local(cmd)
|
|
36
|
+
data = _maybe_enrich_screenshot(cmd, data)
|
|
37
|
+
return CommandResult.success(action=cmd.action, data=data, command=cmd.line, meta=meta)
|
|
38
|
+
except VDisplayError as exc:
|
|
39
|
+
return CommandResult.failure(
|
|
40
|
+
action=cmd.action,
|
|
41
|
+
error=error_from_exception(exc),
|
|
42
|
+
command=cmd.line,
|
|
43
|
+
meta=meta,
|
|
44
|
+
)
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
return CommandResult.failure(
|
|
47
|
+
action=cmd.action,
|
|
48
|
+
error=error_from_exception(exc),
|
|
49
|
+
command=cmd.line,
|
|
50
|
+
meta=meta,
|
|
51
|
+
)
|