|
| 1 | +import base64 |
1 | 2 | import logging |
2 | 3 | import pathlib |
3 | 4 | import subprocess |
|
43 | 44 | DeleteRenderObjectCommand, |
44 | 45 | GetActiveProcessCommand, |
45 | 46 | GetActiveWindowCommand, |
| 47 | + GetFileCommand, |
| 48 | + GetFileNamesCommand, |
46 | 49 | GetMousePositionCommand, |
47 | 50 | GetSystemInfoCommand, |
48 | 51 | Guid, |
|
51 | 54 | Location, |
52 | 55 | Message, |
53 | 56 | Parameter3, |
| 57 | + RemoveVirtualDisplaysCommand, |
54 | 58 | RenderImage, |
55 | 59 | RenderObjectId, |
56 | 60 | RenderObjectStyle, |
|
66 | 70 | GetActiveProcessResponseModel, |
67 | 71 | GetActiveWindowResponse, |
68 | 72 | GetActiveWindowResponseModel, |
| 73 | + GetFileNamesResponse, |
| 74 | + GetFileResponse, |
69 | 75 | GetSystemInfoResponse, |
70 | 76 | GetSystemInfoResponseModel, |
71 | 77 | ) |
72 | 78 | from askui.utils.annotated_image import AnnotatedImage |
| 79 | +from askui.utils.image_utils import base64_to_image |
73 | 80 |
|
74 | 81 | from ..utils import process_exists, wait_for_port |
75 | 82 | from .exceptions import ( |
@@ -217,6 +224,12 @@ def connect(self) -> None: |
217 | 224 | self._start_session() |
218 | 225 | self._start_execution() |
219 | 226 | self.set_display(self._display) |
| 227 | + if self._settings.clean_virtual_displays: |
| 228 | + logger.info( |
| 229 | + "clean_virtual_displays is enabled. Removing all virtual displays ... " |
| 230 | + ) |
| 231 | + self.remove_virtual_displays() |
| 232 | + logger.info("Virtual displays removed.") |
220 | 233 |
|
221 | 234 | def _get_stub(self) -> controller_v1.ControllerAPIStub: |
222 | 235 | assert isinstance(self._stub, controller_v1.ControllerAPIStub), ( |
@@ -1294,3 +1307,109 @@ def set_window_in_focus(self, process_id: int, window_id: int) -> None: |
1294 | 1307 | _window_id = Parameter3(root=window_id) |
1295 | 1308 | command = SetActiveWindowCommand(parameters=[_process_id, _window_id]) |
1296 | 1309 | self._send_command(command) |
| 1310 | + |
| 1311 | + def get_file_names(self, absolute_directory_path: str) -> list[str]: |
| 1312 | + """ |
| 1313 | + Get the file names in the given absolute directory on the device under |
| 1314 | + automation. |
| 1315 | +
|
| 1316 | + Args: |
| 1317 | + absolute_directory_path (str): The absolute directory path to list |
| 1318 | + file names from. |
| 1319 | +
|
| 1320 | + Returns: |
| 1321 | + list[str]: The file names returned by the controller. |
| 1322 | + """ |
| 1323 | + assert isinstance(self._stub, controller_v1.ControllerAPIStub), ( |
| 1324 | + "Stub is not initialized" |
| 1325 | + ) |
| 1326 | + self._reporter.add_message( |
| 1327 | + "AgentOS", f"get_file_names({absolute_directory_path})" |
| 1328 | + ) |
| 1329 | + command = GetFileNamesCommand(parameters=[absolute_directory_path]) |
| 1330 | + res = self._send_command(command).message.command |
| 1331 | + if not isinstance(res, GetFileNamesResponse): |
| 1332 | + message = f"unexpected response type: {res}" |
| 1333 | + raise DesktopAgentOsError(message) |
| 1334 | + if res.error is not None: |
| 1335 | + raise DesktopAgentOsError(res.error) |
| 1336 | + if res.response is None: |
| 1337 | + message = f"{type(res).__name__} is missing both error and response" |
| 1338 | + raise DesktopAgentOsError(message) |
| 1339 | + self._reporter.add_message( |
| 1340 | + "AgentOS", f"get_file_names({absolute_directory_path}) -> {res.response}" |
| 1341 | + ) |
| 1342 | + return res.response.fileNames |
| 1343 | + |
| 1344 | + def get_file(self, path: str) -> Image.Image | str: |
| 1345 | + """ |
| 1346 | + Get the contents of a file at the given path on the device under |
| 1347 | + automation. |
| 1348 | +
|
| 1349 | + The controller returns the file as a Base64-encoded string, which is |
| 1350 | + decoded and returned as `PIL.Image.Image` when the bytes can be opened |
| 1351 | + as an image (PNG, JPEG, BMP, GIF, WebP, TIFF, ...), or as `str` when |
| 1352 | + they decode cleanly as UTF-8 text. |
| 1353 | +
|
| 1354 | + Args: |
| 1355 | + path (str): The file path to read on the device under automation. |
| 1356 | +
|
| 1357 | + Returns: |
| 1358 | + Image.Image | str: The decoded file contents. |
| 1359 | +
|
| 1360 | + Raises: |
| 1361 | + DesktopAgentOsError: If the file cannot be read or the response is invalid. |
| 1362 | + """ |
| 1363 | + assert isinstance(self._stub, controller_v1.ControllerAPIStub), ( |
| 1364 | + "Stub is not initialized" |
| 1365 | + ) |
| 1366 | + self._reporter.add_message("AgentOS", f"get_file({path})") |
| 1367 | + command = GetFileCommand(parameters=[path]) |
| 1368 | + res = self._send_command(command).message.command |
| 1369 | + if not isinstance(res, GetFileResponse): |
| 1370 | + message = f"unexpected response type: {res}" |
| 1371 | + raise DesktopAgentOsError(message) |
| 1372 | + if res.error is not None: |
| 1373 | + raise DesktopAgentOsError(res.error) |
| 1374 | + if res.response is None: |
| 1375 | + message = f"{type(res).__name__} is missing both error and response" |
| 1376 | + raise DesktopAgentOsError(message) |
| 1377 | + decoded = self._decode_file_payload(res.response.file.content) |
| 1378 | + if isinstance(decoded, Image.Image): |
| 1379 | + detail = f"image ({decoded.format}, {decoded.size[0]}x{decoded.size[1]})" |
| 1380 | + self._reporter.add_message( |
| 1381 | + "AgentOS", f"get_file({path}) -> {detail}", decoded |
| 1382 | + ) |
| 1383 | + return decoded |
| 1384 | + |
| 1385 | + detail = f"text ({len(decoded)} chars)" |
| 1386 | + self._reporter.add_message("AgentOS", f"get_file({path}) -> {detail}") |
| 1387 | + return decoded |
| 1388 | + |
| 1389 | + def remove_virtual_displays(self) -> None: |
| 1390 | + """ |
| 1391 | + Remove all virtual displays from the controller, leaving only real |
| 1392 | + displays active. |
| 1393 | + """ |
| 1394 | + assert isinstance(self._stub, controller_v1.ControllerAPIStub), ( |
| 1395 | + "Stub is not initialized" |
| 1396 | + ) |
| 1397 | + self._reporter.add_message("AgentOS", "remove_virtual_displays()") |
| 1398 | + command = RemoveVirtualDisplaysCommand() |
| 1399 | + self._send_command(command) |
| 1400 | + self._reporter.add_message("AgentOS", "remove_virtual_displays() -> done") |
| 1401 | + |
| 1402 | + @staticmethod |
| 1403 | + def _decode_file_payload(base64_data: str) -> Image.Image | str: |
| 1404 | + try: |
| 1405 | + return base64_to_image(base64_data) |
| 1406 | + except ValueError: |
| 1407 | + pass |
| 1408 | + data = base64.b64decode(base64_data, validate=True) |
| 1409 | + if b"\x00" not in data: |
| 1410 | + try: |
| 1411 | + return data.decode("utf-8") |
| 1412 | + except UnicodeDecodeError: |
| 1413 | + pass |
| 1414 | + message = "File contents are neither a supported image nor UTF-8 text" |
| 1415 | + raise DesktopAgentOsError(message) |
0 commit comments