Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ dependencies:
- robocorp-inspector==0.10.2 # https://github.com/robocorp/inspector/blob/master/CHANGELOG.md
- robotframework==5.0.1 # https://github.com/robotframework/robotframework/blob/master/doc/releasenotes/rf-5.0.1.rst
- playwright==1.37.0
- java-access-bridge-wrapper=="1.1.0"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this windows only?

65 changes: 45 additions & 20 deletions robocorp-code/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions robocorp-code/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pillow = "^9.0"
pywin32 = { version = ">=300,<304", platform = "win32", python = "!=3.8.1" }
psutil = "^5.9.0"
comtypes = "^1.1"
java-access-bridge-wrapper = "^1.1.0"
ruff = "^0.2.1"

[tool.poetry.group.dev.dependencies]
robocorp-python-ls-core = {path = "../robocorp-python-ls-core/", develop = true}
Expand All @@ -52,7 +54,6 @@ robocorp-actions = "^0.0.7" # Just needed for testing.

numpy = "<2"
black = "^23.1.0"
ruff = "^0.0.255"
mypy = "^1.1.1"
isort = { version = "^5.12.0", python = "^3.8" }
invoke = "^2.0"
Expand Down Expand Up @@ -89,7 +90,7 @@ exclude = "vendored"
[tool.ruff]
ignore = [
"E501", # Line-len.
"F541", # f-string without placeholders.
"F541", # f-string without placeholders.
"E731", # Use 'def' instead of lambda.
]

Expand Down
Empty file.
96 changes: 96 additions & 0 deletions robocorp-code/src/robocorp_code/inspector/java/java_inspector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from typing import List, Optional, TypedDict, cast

from JABWrapper.context_tree import ContextNode # type: ignore
from JABWrapper.jab_wrapper import JavaWindow # type: ignore
from robocorp_ls_core.robotframework_log import get_logger

from robocorp_code.inspector.java.robocorp_java._inspector import ColletedTreeTypedDict

log = get_logger(__name__)

JavaWindowInfoTypedDict = TypedDict(
"JavaWindowInfoTypedDict",
{
"pid": int,
"hwnd": int,
"title": str,
},
)

LocatorNodeInfoTypedDict = TypedDict(
"LocatorNodeInfoTypedDict",
{
"name": str,
"role": str,
"description": str,
"states": str,
"indexInParent": int,
"childrenCount": int,
"x": int,
"y": int,
"width": int,
"height": int,
"ancestry": int,
},
)


class MatchesAndHierarchyTypedDict(TypedDict):
# A list with the nodes matched (these are the ones that the
# locator matched)
matched_paths: List[str]
# This includes all the entries found along with the full hierarchy
# to reach the matched entries.
hierarchy: List[LocatorNodeInfoTypedDict]


def to_window_info(java_window: JavaWindow) -> JavaWindowInfoTypedDict:
ret = {}
for dct_name in JavaWindowInfoTypedDict.__annotations__:
ret[dct_name] = getattr(java_window, dct_name)
return cast(JavaWindowInfoTypedDict, ret)


def to_locator_info(context_node: ContextNode) -> LocatorNodeInfoTypedDict:
ret = {}
for dct_name in LocatorNodeInfoTypedDict.__annotations__:
if (dct_name) == "ancestry":
ret["ancestry"] = getattr(context_node, dct_name)
else:
ret[dct_name] = getattr(context_node.context_info, dct_name)
return cast(LocatorNodeInfoTypedDict, ret)


def to_matches_and_hierarchy(
matches_and_hierarchy: ColletedTreeTypedDict,
) -> MatchesAndHierarchyTypedDict:
matches = (
[str(matches_and_hierarchy["matches"])]
if type(matches_and_hierarchy["matches"]) == ContextNode
else [str(match) for match in matches_and_hierarchy["matches"]]
)
hierarchy = [to_locator_info(node) for node in matches_and_hierarchy["tree"]]
return {"matched_paths": matches, "hierarchy": hierarchy}


class JavaInspector:
def __init__(self):
from robocorp_code.inspector.java.robocorp_java._inspector import (
ElementInspector,
)

self._inspector = ElementInspector()

def list_windows(self) -> List[JavaWindowInfoTypedDict]:
windows = self._inspector.list_windows()
return [to_window_info(window) for window in windows]

def collect_tree(
self, window: str, search_depth=1, locator: Optional[str] = None
) -> MatchesAndHierarchyTypedDict:
log.info(f"Collect tree from locator: {locator}")

matches_and_hierarchy = self._inspector.collect_tree(
window, search_depth, locator
)
return to_matches_and_hierarchy(matches_and_hierarchy)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class NoMatchingLocatorException(Exception):
"""Match for locator not found."""


class ContextNotAvailable(Exception):
"""The Java context has not been created yet."""


class LocatorNotProvidedException(Exception):
"""Locator not provided."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import ctypes
import platform
import threading
import time
from concurrent import futures

from JABWrapper.jab_wrapper import JavaAccessBridgeWrapper # type: ignore
from robocorp_ls_core.robotframework_log import get_logger

log = get_logger(__name__)


REMOVE_FROM_QUEUE = 0x0001


class EventPumpThread(threading.Thread):
def __init__(
self,
) -> None:
super().__init__()
# Jab wrapper needs to be part of the thread that pumps the window events
self._jab_wrapper: JavaAccessBridgeWrapper = None
self._future: futures.Future = futures.Future()
self._quit_event_loop = threading.Event()

def _pump_background(self) -> bool:
try:
PeekMessage = ctypes.windll.user32.PeekMessageW # type: ignore
TranslateMessage = ctypes.windll.user32.TranslateMessage # type: ignore
DispatchMessage = ctypes.windll.user32.DispatchMessageW # type: ignore

message = ctypes.byref(ctypes.wintypes.MSG())
# Nonblocking API to get windows window events from the queue.
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-peekmessagea
# Use non blocing API here so that the thread can quit.
if PeekMessage(message, 0, 0, 0, REMOVE_FROM_QUEUE):
TranslateMessage(message)
log.debug("Dispatching msg={}".format(repr(message)))
DispatchMessage(message)
return True
except Exception as err:
log.error(f"Pump error: {err}")
finally:
log.info("Stopped processing events")
return False

def run(self) -> None:
if platform.system() != "Windows":
return

# Raise the error to the main thread from here
self._jab_wrapper = JavaAccessBridgeWrapper(ignore_callbacks=True)
self._future.set_result(self._jab_wrapper)
while not self._quit_event_loop.is_set():
# The pump is non blocking. If the is no message in the queue
# wait for 10 milliseconds until check again to prevent too
# fast loop.
# TODO: add backoff timer
if not self._pump_background():
time.sleep(0.01)

def stop(self):
self._quit_event_loop.set()
self._jab_wrapper = None
if not self._future.done():
self._future.cancel()

def get_wrapper(self) -> JavaAccessBridgeWrapper:
return self._future.result()
Loading