Skip to content

Commit affc7bd

Browse files
Merge pull request #562 from AutomationSolutionz/saimon_mac_appium
Appium(mac & ios) action create and modification Mac inspector added
2 parents c4f02ec + 31e45ed commit affc7bd

File tree

12 files changed

+1453
-264
lines changed

12 files changed

+1453
-264
lines changed

.vscode/settings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"files.readonlyInclude": {
66
"**/.git": true,
77
"**/node_modules": true,
8-
"Framework/settings.conf": true,
98
"node_state.json": true,
109
"pid.txt": true
1110
}

Apps/Mac/inspector.py

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import sys
2+
import os
3+
from textwrap import dedent
4+
import requests
5+
import json
6+
from configobj import ConfigObj
7+
from pathlib import Path
8+
import traceback
9+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
10+
11+
print(f"Python Version: {sys.version}")
12+
print(f"Python Path: {sys.executable}")
13+
print(f"Current file path: {os.path.abspath(__file__)}")
14+
15+
from rich import print as rich_print
16+
from rich.text import Text
17+
from rich.tree import Tree
18+
from colorama import Fore, init as colorama_init
19+
colorama_init(autoreset=True)
20+
21+
import ctypes
22+
import objc
23+
from Foundation import NSObject
24+
from Quartz import (
25+
CoreGraphics,
26+
CGEventSourceFlagsState,
27+
kCGEventSourceStateHIDSystemState,
28+
kCGEventFlagMaskControl,
29+
CGWindowListCopyWindowInfo,
30+
kCGWindowListOptionOnScreenOnly,
31+
kCGNullWindowID
32+
)
33+
from AppKit import NSEvent, NSControlKeyMask
34+
import time
35+
36+
import xml.etree.ElementTree as ET
37+
38+
# AXUIElement types
39+
AXUIElementRef = objc.objc_object
40+
41+
# Load the AX API
42+
ApplicationServices = objc.loadBundle("ApplicationServices",
43+
globals(),
44+
bundle_path="/System/Library/Frameworks/ApplicationServices.framework"
45+
)
46+
# AX, _ = objc.loadBundleFunctions(ApplicationServices, globals(), [
47+
# ("AXUIElementCreateApplication", b"^{__AXUIElement=}(i)")
48+
# ])
49+
50+
AX = objc.loadBundleFunctions(ApplicationServices, globals(), [
51+
("AXUIElementCreateSystemWide", b"^{__AXUIElement=}"),
52+
("kAXFocusedUIElementAttribute", b"^{__CFString=}"),
53+
("AXUIElementCopyAttributeValue", b"i^{__AXUIElement=}^{__CFString=}^@"),
54+
("AXUIElementCopyAttributeNames", b"i^{__AXUIElement=}^{__CFArray=}"),
55+
("AXUIElementCopyElementAtPosition", b"i^{__AXUIElement=}dd^@"),
56+
("AXUIElementCreateApplication", b"^{__AXUIElement=}" + b"i")
57+
])
58+
59+
settings_conf_path = str(Path(__file__).parent.parent.parent / "Framework" / "settings.conf")
60+
print(f"Settings config path: {settings_conf_path}")
61+
62+
def get_mouse_position():
63+
event = CoreGraphics.CGEventCreate(None)
64+
loc = CoreGraphics.CGEventGetLocation(event)
65+
x, y = round(loc.x), round(loc.y)
66+
return x, y
67+
68+
69+
class App:
70+
def __init__(self, name: str, bundle_id: str, pid: int, window_title: str):
71+
self.name = name
72+
self.bundle_id = bundle_id
73+
self.pid = pid
74+
self.window_title = window_title
75+
76+
def __str__(self):
77+
return Fore.GREEN + dedent(f"""
78+
App(
79+
name={self.name},
80+
bundle_id={self.bundle_id},
81+
pid={self.pid},
82+
window_title={self.window_title},
83+
)""")
84+
85+
class Inspector:
86+
def __init__(self):
87+
self.x: int = -1
88+
self.y: int = -1
89+
self.app: App = App(name="", bundle_id="", pid=-1, window_title="")
90+
self.xml_str: str = ""
91+
self.xml_tree: ET.ElementTree = None
92+
93+
self.server_address: str = "http://127.0.0.1"
94+
self.server_path: str = "/api/v1/mac/dump/driver"
95+
self.server_port: int = 18100
96+
self.page_src: str = ""
97+
def wait_for_control_press(self):
98+
print("Hover over the element and press ⌃ Control key...")
99+
while True:
100+
flags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState)
101+
if flags & kCGEventFlagMaskControl:
102+
point = NSEvent.mouseLocation()
103+
height = NSScreen.mainScreen().frame().size.height
104+
x = round(point.x)
105+
y = round(height - point.y)
106+
rich_print(f"Captured at x={x}, y={y}")
107+
self.x, self.y = x, y
108+
return
109+
time.sleep(0.1)
110+
111+
def get_frontmost_app(self):
112+
window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID)
113+
for window in window_list:
114+
if window.get("kCGWindowLayer") == 0 and window.get("kCGWindowOwnerName"):
115+
app_name = window["kCGWindowOwnerName"]
116+
pid = window["kCGWindowOwnerPID"]
117+
app = NSRunningApplication.runningApplicationWithProcessIdentifier_(pid)
118+
bundle_id = app.bundleIdentifier()
119+
window_title = window.get("kCGWindowName", "")
120+
self.app = App(name=app_name, bundle_id=bundle_id, pid=pid, window_title=window_title)
121+
print(self.app)
122+
break
123+
124+
def get_server_port(self):
125+
config = ConfigObj(settings_conf_path)
126+
self.server_port = config["server"]["port"]
127+
128+
def get_dump(self):
129+
url = f"{self.server_address}:{self.server_port}{self.server_path}"
130+
try:
131+
response = requests.get(url).json()
132+
except requests.exceptions.ConnectionError:
133+
print(Fore.RED + "Failed to connect to the server. Please launch the Zeuz Node first and launch an app")
134+
return
135+
if response["status"] == "ok":
136+
self.page_src = response["ui_xml"]
137+
print(Fore.GREEN + f"Successfully got dump from appium driver")
138+
elif response["status"] == "not_found":
139+
print(Fore.GREEN + f"You have not launched any app yet. Launch app with the following action:")
140+
action = [
141+
{
142+
"action_name":f"Launch {self.app.name}",
143+
"action_disabled":"true",
144+
"step_actions":[
145+
["macos app bundle id","element parameter",self.app.bundle_id],
146+
["launch","appium action","launch"]
147+
]
148+
}
149+
]
150+
print(Fore.CYAN + json.dumps(action, indent=4))
151+
self.page_src = ""
152+
else:
153+
print(Fore.RED + f"Error: {response['error']}")
154+
self.page_src = ""
155+
156+
def render_tree(self):
157+
print('rendertree')
158+
if not self.page_src:
159+
return
160+
161+
root = ET.fromstring(self.page_src)
162+
tree = Tree(f"[bold green]{self.app.name} ({self.app.bundle_id})")
163+
self.xml_tree = tree
164+
165+
def check_bounding_box(element):
166+
if element.attrib.get('x') and element.attrib.get('y') and element.attrib.get('width') and element.attrib.get('height'):
167+
x = int(element.attrib.get('x'))
168+
y = int(element.attrib.get('y'))
169+
width = int(element.attrib.get('width'))
170+
height = int(element.attrib.get('height'))
171+
if (self.x >= x and
172+
self.x <= x + width and
173+
self.y >= y and
174+
self.y <= y + height
175+
):
176+
return True
177+
return False
178+
179+
def get_attribute_string(element):
180+
ignore = ['x', 'y', 'width', 'height']
181+
return " ".join([f'{k}="{v}"' for k, v in element.attrib.items() if k not in ignore])
182+
183+
def set_single_zeuz_apiplugin(root):
184+
elements = root.findall(".//*[@zeuz='aiplugin']")
185+
if len(elements) > 1:
186+
element_areas = []
187+
for element in elements:
188+
width = int(element.attrib.get('width', 0))
189+
height = int(element.attrib.get('height', 0))
190+
area = width * height
191+
element_areas.append((element, area))
192+
193+
element_areas.sort(key=lambda x: x[1])
194+
for element, _ in element_areas[1:]:
195+
del element.attrib['zeuz']
196+
197+
def remove_coordinates(node):
198+
remove = ['x', 'y', 'width', 'height']
199+
for child in node:
200+
for attrib in list(child.attrib):
201+
if attrib in remove:
202+
del child.attrib[attrib]
203+
remove_coordinates(child)
204+
205+
def build_tree(element, parent_tree):
206+
element_tag = element.tag
207+
ignore = ['x', 'y', 'width', 'height']
208+
element_attribs = get_attribute_string(element)
209+
element_coords = f"x={element.attrib.get('x', '')}, y={element.attrib.get('y', '')}, w={element.attrib.get('width', '')}, h={element.attrib.get('height', '')}"
210+
recorded_coords = f"self.x={self.x}, self.y={self.y}"
211+
212+
if check_bounding_box(element):
213+
if not any(check_bounding_box(child) for child in element):
214+
area = int(element.attrib.get('width', '1')) * int(element.attrib.get('height', '1'))
215+
label = f"[bold blue]{element_tag}: [green]{element_attribs} [dim]({element_coords} Area: {area} {recorded_coords})"
216+
element.set('zeuz', 'aiplugin')
217+
else:
218+
label = f"[bold blue]{element_tag}: [yellow]{element_attribs}"
219+
220+
else:
221+
label = f"[bold]{element_tag}: {element_attribs}"
222+
node = parent_tree.add(label)
223+
224+
for child in element:
225+
if check_bounding_box(child):
226+
build_tree(child, node)
227+
else:
228+
node.add(f"[bold]{child.tag}: {get_attribute_string(child)}")
229+
230+
build_tree(root, tree)
231+
set_single_zeuz_apiplugin(root)
232+
rich_print(tree)
233+
remove_coordinates(root)
234+
self.xml_str = ET.tostring(root).decode().encode('ascii', 'ignore').decode()
235+
236+
237+
''' Comment out the below code to check if tree contains single zeuz apiplugin '''
238+
# tree2 = Tree(f"[bold green]{self.app.name} ({self.app.bundle_id})")
239+
# build_tree(root, tree2)
240+
# rich_print(tree2)
241+
def send_to_server(self):
242+
config = ConfigObj(settings_conf_path)
243+
api_key = config["Authentication"]["api-key"].strip()
244+
server = config["Authentication"]["server_address"].strip()
245+
246+
if not api_key or not server:
247+
print(Fore.RED + "API key or server address is not set. Please launch the Zeuz Node first and login")
248+
return
249+
url = f"{self.server_address}:{self.server_port}{self.server_path}"
250+
try:
251+
url = server + "/" if server[-1] != "/" else server
252+
url += "ai_record_single_action/"
253+
print(url)
254+
content = json.dumps({
255+
'page_src': self.xml_str,
256+
"action_type": "android",
257+
})
258+
headers = {
259+
"X-Api-Key": api_key,
260+
}
261+
262+
r = requests.request("POST", url, headers=headers, data=content, verify=False)
263+
response = r.json()
264+
if response["info"] == "success":
265+
r.ok and print("Element sent. You can " + Fore.GREEN + "'Add by AI' " + Fore.RESET + "from server")
266+
else:
267+
print(Fore.RED + response["info"])
268+
except:
269+
traceback.print_exc()
270+
print(Fore.RED + "Failed to send content to AI Engine")
271+
return
272+
273+
def run(self):
274+
while True:
275+
input("Press any key to start capturing...")
276+
self.wait_for_control_press()
277+
self.get_frontmost_app()
278+
self.get_server_port()
279+
if self.server_port == 0:
280+
print(Fore.RED + "Server port is not set. Please launch the Zeuz Node first and launch an app")
281+
continue
282+
self.get_dump()
283+
if not self.page_src:
284+
continue
285+
self.render_tree()
286+
self.send_to_server()
287+
288+
time.sleep(0.2)
289+
290+
291+
def main():
292+
inspector = Inspector()
293+
inspector.run()
294+
295+
if __name__ == "__main__":
296+
main()

0 commit comments

Comments
 (0)