Skip to content

Commit d2ba0e1

Browse files
committed
tmp
1 parent 1e30bcd commit d2ba0e1

6 files changed

Lines changed: 230 additions & 4 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,5 @@ custom_profiles/
6161
/tools/security/*
6262
Apps/Windows/inspector.exe
6363
Apps/Windows/Element.xml
64-
Framework/settings.conf.lock
64+
Framework/settings.conf.lock
65+
Framework/Built_In_Automation/Desktop/Linux/latest_app.txt

Framework/Built_In_Automation/Desktop/Linux/BuiltInFunctions.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,47 @@ def queryValue(self) -> Value: ...
101101

102102
MODULE_NAME = inspect.getmodulename(__file__) or "BuiltInFunctions"
103103
ui_xml_strings = [] # needed for generating XML tree
104+
LATEST_APP_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "latest_app.txt")
105+
106+
107+
def save_latest_app_name(app_name: str):
108+
"""Save the latest used application name to a file."""
109+
try:
110+
with open(LATEST_APP_FILE, "w") as f:
111+
f.write(app_name)
112+
except Exception as e:
113+
CommonUtil.ExecLog(MODULE_NAME, f"Failed to save latest app name: {e}", 3)
114+
115+
116+
def get_latest_app_name() -> str | None:
117+
"""Get the latest used application name from the file."""
118+
try:
119+
if os.path.exists(LATEST_APP_FILE):
120+
with open(LATEST_APP_FILE, "r") as f:
121+
return f.read().strip()
122+
except Exception as e:
123+
CommonUtil.ExecLog(MODULE_NAME, f"Failed to get latest app name: {e}", 3)
124+
return None
125+
126+
127+
def capture_screenshot(file_path: str) -> bool:
128+
"""Capture screenshot using available tools (scrot, gnome-screenshot, import)."""
129+
tools = [
130+
["scrot", file_path],
131+
["gnome-screenshot", "-f", file_path],
132+
["import", "-window", "root", file_path]
133+
]
134+
135+
for cmd in tools:
136+
try:
137+
subprocess.run(cmd, check=True, capture_output=True)
138+
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
139+
return True
140+
except (subprocess.CalledProcessError, FileNotFoundError):
141+
continue
142+
143+
CommonUtil.ExecLog(MODULE_NAME, "Failed to capture screenshot. Ensure scrot, gnome-screenshot, or imagemagick is installed.", 3)
144+
return False
104145

105146

106147
def convert_data_set_to_dict(data_set: DataSet) -> dict[str, str]:
@@ -597,6 +638,7 @@ def open_app(data_set: DataSet) -> Literal["passed", "zeuz_failed"]:
597638
exit_code = os.system(command)
598639
if exit_code == 0:
599640
CommonUtil.ExecLog(sModuleInfo, f"Successfully launched '{app_name}' with command: {command}", 1)
641+
save_latest_app_name(app_name)
600642
return "passed"
601643
else:
602644
CommonUtil.ExecLog(sModuleInfo, f"Failed to launch '{app_name}' (exit code: {exit_code})", 3)
@@ -611,6 +653,7 @@ def open_app(data_set: DataSet) -> Literal["passed", "zeuz_failed"]:
611653
exit_code = os.system(command)
612654
if exit_code == 0:
613655
CommonUtil.ExecLog(sModuleInfo, f"Successfully launched '{app_name}' with command: {command}", 1)
656+
save_latest_app_name(app_name)
614657
return "passed"
615658
else:
616659
CommonUtil.ExecLog(sModuleInfo, f"Failed to launch '{app_name}' (exit code: {exit_code})", 3)

Framework/nodejs_appium_installer.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import subprocess
55
import tarfile
66
import zipfile
7+
import shutil
78
from pathlib import Path
89
import json
910
import requests
@@ -99,7 +100,7 @@ def install_nodejs():
99100
print("Downloading Node.js...")
100101
response = requests.get(url, verify=False)
101102
response.raise_for_status()
102-
with open(archive_path, 'wb') as out_file:
103+
with open(archive_path, "wb") as out_file:
103104
out_file.write(response.content)
104105

105106
try:
@@ -196,7 +197,9 @@ def check_appium_drivers():
196197
text=True,
197198
)
198199
drivers_data = json.loads(result.stdout)
199-
return [name for name, info in drivers_data.items() if info.get("installed", False)]
200+
return [
201+
name for name, info in drivers_data.items() if info.get("installed", False)
202+
]
200203
except: # noqa: E722
201204
return []
202205

@@ -213,7 +216,9 @@ def check_installations():
213216
if npm_path.exists():
214217
try:
215218
result = subprocess.run(
216-
[str(npm_path), "list", "-g", "--json", "appium"], capture_output=True, text=True
219+
[str(npm_path), "list", "-g", "--json", "appium"],
220+
capture_output=True,
221+
text=True,
217222
)
218223
npm_data = json.loads(result.stdout)
219224
appium_installed = "appium" in npm_data.get("dependencies", {})
@@ -234,9 +239,73 @@ def install_missing_drivers(missing_drivers):
234239
install_drivers(missing_drivers)
235240

236241

242+
def check_and_remove_global_appium():
243+
"""Check for and remove existing global Appium installations not managed by us."""
244+
print("Checking for conflicting global Appium installations...")
245+
246+
# Method 1: Check using 'which appium'
247+
appium_bin = shutil.which("appium")
248+
if appium_bin:
249+
appium_path = Path(appium_bin).resolve()
250+
node_dir = get_node_dir().resolve()
251+
252+
try:
253+
# Check if appium is within our node directory
254+
appium_path.relative_to(node_dir)
255+
# If it is, we are good
256+
except ValueError:
257+
print(f"Found conflicting Appium at {appium_path}")
258+
print("Uninstalling old Appium version...")
259+
try:
260+
is_windows = platform.system() == "Windows"
261+
subprocess.run(
262+
["npm", "uninstall", "-g", "appium"], check=True, shell=is_windows
263+
)
264+
print("Successfully uninstalled conflicting Appium")
265+
except Exception as e:
266+
print(f"Warning: Failed to uninstall conflicting Appium: {e}")
267+
return
268+
269+
# Method 2: Check using 'npm list -g appium' (if npm is available in system path)
270+
# This catches cases where appium is installed but not in PATH
271+
npm_bin = shutil.which("npm")
272+
if npm_bin:
273+
try:
274+
# Check if this npm is ours
275+
npm_path = Path(npm_bin).resolve()
276+
node_dir = get_node_dir().resolve()
277+
try:
278+
npm_path.relative_to(node_dir)
279+
# If it is our npm, skip this check as we handle our own appium
280+
return
281+
except ValueError:
282+
pass
283+
284+
# Check for global appium using system npm
285+
is_windows = platform.system() == "Windows"
286+
result = subprocess.run(
287+
["npm", "list", "-g", "--json", "appium"],
288+
capture_output=True,
289+
text=True,
290+
shell=is_windows,
291+
)
292+
npm_data = json.loads(result.stdout)
293+
if "appium" in npm_data.get("dependencies", {}):
294+
print("Found conflicting Appium in global npm modules")
295+
print("Uninstalling old Appium version...")
296+
subprocess.run(
297+
["npm", "uninstall", "-g", "appium"], check=True, shell=is_windows
298+
)
299+
print("Successfully uninstalled conflicting Appium")
300+
except Exception as e:
301+
# Don't fail if npm check fails, just log warning
302+
print(f"Warning: Failed to check/uninstall global Appium via npm: {e}")
303+
304+
237305
def setup_nodejs_appium():
238306
"""Main setup function."""
239307
try:
308+
check_and_remove_global_appium()
240309
update_path() # Ensure Node.js is in PATH from the start
241310

242311
print("Checking Node.js and Appium installation...")

Installer/setup_linux_inspector.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ APT_PACKAGES=(
1010
libgirepository1.0-dev
1111
libcairo2-dev
1212
xdotool
13+
scrot
1314
)
1415

1516
DNF_PACKAGES=(
@@ -20,6 +21,7 @@ DNF_PACKAGES=(
2021
xdotool
2122
python3-devel
2223
cairo-gobject-devel
24+
scrot
2325
)
2426

2527
PACMAN_PACKAGES=(
@@ -30,6 +32,7 @@ PACMAN_PACKAGES=(
3032
cairo
3133
xdotool
3234
gobject-introspection
35+
scrot
3336
)
3437

3538
BREW_PACKAGES=(
@@ -38,6 +41,7 @@ BREW_PACKAGES=(
3841
cairo
3942
xdotool
4043
gobject-introspection
44+
scrot
4145
)
4246

4347
# Function to join array into space-separated string

server/linux.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import hashlib
2+
import os
3+
import base64
4+
import asyncio
5+
import requests
6+
from typing import Literal
7+
from fastapi import APIRouter
8+
from pydantic import BaseModel
9+
10+
from Framework.Utilities import ConfigModule, CommonUtil
11+
12+
try:
13+
from Framework.Built_In_Automation.Desktop.Linux import BuiltInFunctions
14+
except ImportError:
15+
BuiltInFunctions = None
16+
17+
router = APIRouter(prefix="/linux", tags=["linux"])
18+
19+
SCREENSHOT_PATH = "linux_screen.png"
20+
21+
class InspectorResponse(BaseModel):
22+
"""Response model for the /inspector endpoint."""
23+
status: Literal["ok", "error"] = "ok"
24+
ui_xml: str | None = None
25+
screenshot: str | None = None # Base64 encoded image
26+
error: str | None = None
27+
28+
29+
@router.get("/inspect")
30+
def inspect(app_name: str | None = None):
31+
"""Get the Linux UI DOM and screenshot."""
32+
if BuiltInFunctions is None:
33+
return InspectorResponse(status="error", error="Linux automation module not available")
34+
35+
try:
36+
# Determine app name
37+
target_app = app_name
38+
if not target_app:
39+
target_app = BuiltInFunctions.get_latest_app_name()
40+
41+
if not target_app:
42+
return InspectorResponse(status="error", error="No application specified and no latest app found.")
43+
44+
# Capture UI
45+
xml_content = BuiltInFunctions.get_ui_tree(target_app)
46+
if not xml_content:
47+
return InspectorResponse(status="error", error=f"Failed to get UI tree for app: {target_app}")
48+
49+
# Capture Screenshot
50+
full_screenshot_path = os.path.abspath(SCREENSHOT_PATH)
51+
52+
screenshot_base64 = None
53+
if BuiltInFunctions.capture_screenshot(full_screenshot_path):
54+
try:
55+
with open(full_screenshot_path, 'rb') as img_file:
56+
screenshot_bytes = img_file.read()
57+
screenshot_base64 = base64.b64encode(screenshot_bytes).decode('utf-8')
58+
except Exception:
59+
pass
60+
61+
return InspectorResponse(
62+
status="ok",
63+
ui_xml=xml_content,
64+
screenshot=screenshot_base64
65+
)
66+
except Exception as e:
67+
return InspectorResponse(
68+
status="error",
69+
error=str(e)
70+
)
71+
72+
73+
async def upload_linux_ui_dump():
74+
"""Continuously upload Linux UI dump if changed."""
75+
if BuiltInFunctions is None:
76+
return
77+
78+
prev_xml_hash = ""
79+
while True:
80+
try:
81+
target_app = BuiltInFunctions.get_latest_app_name()
82+
if target_app:
83+
xml_content = BuiltInFunctions.get_ui_tree(target_app)
84+
if xml_content:
85+
new_xml_hash = hashlib.sha256(xml_content.encode('utf-8')).hexdigest()
86+
87+
if prev_xml_hash != new_xml_hash:
88+
prev_xml_hash = new_xml_hash
89+
90+
url = ConfigModule.get_config_value("Authentication", "server_address").strip() + "/node_ai_contents/"
91+
apiKey = ConfigModule.get_config_value("Authentication", "api-key").strip()
92+
93+
res = requests.post(
94+
url,
95+
headers={"X-Api-Key": apiKey},
96+
json={
97+
"dom_linux": {"dom": xml_content},
98+
"node_id": CommonUtil.MachineInfo().getLocalUser().lower(),
99+
"app_name": target_app # Extra info might be useful
100+
}
101+
)
102+
if res.ok:
103+
CommonUtil.ExecLog("", "Linux UI dump uploaded successfully", iLogLevel=1)
104+
except Exception as e:
105+
CommonUtil.ExecLog("", f"Error uploading Linux UI dump: {str(e)}", iLogLevel=3)
106+
107+
await asyncio.sleep(5)

server/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from server.node_operator import router as operator_router
1111
from server.mobile import router as mobile_router, upload_android_ui_dump
1212
from server.mac import router as mac_router
13+
from server.linux import router as linux_router
1314
import asyncio
1415

1516
class EndpointFilter(logging.Filter):
@@ -40,6 +41,7 @@ def main() -> FastAPI:
4041
v1router.include_router(evaluator_router)
4142
v1router.include_router(mobile_router)
4243
v1router.include_router(mac_router)
44+
v1router.include_router(linux_router)
4345
app = FastAPI()
4446
app.include_router(v1router)
4547

0 commit comments

Comments
 (0)