devicely 2.1.7 → 2.1.9
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.
- package/lib/scriptLoader.js +2 -2
- package/lib/server.js +51 -17
- package/package.json +1 -1
- package/scripts/shell/android_device_control.enc +1 -0
- package/scripts/shell/connect_android_usb_multi_final.enc +1 -0
- package/scripts/shell/connect_android_wireless.enc +1 -0
- package/scripts/shell/connect_android_wireless_multi_final.enc +1 -0
- package/scripts/shell/connect_ios_usb_multi_final.enc +1 -0
- package/scripts/shell/connect_ios_wireless_multi_final.enc +1 -0
- package/scripts/shell/ios_device_control.enc +1 -0
- package/scripts/shell/android_device_control.sh +0 -848
- package/scripts/shell/connect_android_usb_multi_final.sh +0 -289
- package/scripts/shell/connect_android_wireless.sh +0 -58
- package/scripts/shell/connect_android_wireless_multi_final.sh +0 -476
- package/scripts/shell/connect_ios_usb_multi_final.sh +0 -4225
- package/scripts/shell/connect_ios_wireless_multi_final.sh +0 -4167
- package/scripts/shell/create_production_scripts.sh +0 -38
- package/scripts/shell/install_uiautomator2.sh +0 -93
- package/scripts/shell/ios_device_control.sh +0 -220
- package/scripts/shell/organize_project.sh +0 -59
- package/scripts/shell/pre-publish-check.sh +0 -238
- package/scripts/shell/publish-to-npm.sh +0 -366
- package/scripts/shell/publish.sh +0 -100
- package/scripts/shell/setup.sh +0 -121
- package/scripts/shell/start.sh +0 -59
- package/scripts/shell/sync-to-npm-package-final.sh +0 -60
- package/scripts/shell/test-local-package.sh +0 -95
- package/scripts/shell/verify-shell-protection.sh +0 -73
|
@@ -1,848 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Android Device Control Script
|
|
3
|
-
# Complete feature parity with iOS control
|
|
4
|
-
# Uses UIAutomator2 for automation
|
|
5
|
-
|
|
6
|
-
# Configuration
|
|
7
|
-
UIAUTOMATOR2_PORT_BASE=6790
|
|
8
|
-
SCRIPT_DIR="$(dirname "$0")"
|
|
9
|
-
|
|
10
|
-
# Function to find config files in multiple locations
|
|
11
|
-
find_config() {
|
|
12
|
-
local filename="$1"
|
|
13
|
-
local search_paths=(
|
|
14
|
-
"$SCRIPT_DIR/$filename"
|
|
15
|
-
"$SCRIPT_DIR/../../config/$filename"
|
|
16
|
-
"$SCRIPT_DIR/../config/$filename"
|
|
17
|
-
"$HOME/.devicely/$filename"
|
|
18
|
-
"/opt/homebrew/lib/node_modules/devicely/config/$filename"
|
|
19
|
-
"$(npm root -g 2>/dev/null)/devicely/config/$filename"
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
for path in "${search_paths[@]}"; do
|
|
23
|
-
if [ -f "$path" ]; then
|
|
24
|
-
echo "$path"
|
|
25
|
-
return 0
|
|
26
|
-
fi
|
|
27
|
-
done
|
|
28
|
-
|
|
29
|
-
return 1
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# Find config files
|
|
33
|
-
CONFIG_FILE=$(find_config "devices.conf")
|
|
34
|
-
if [ -z "$CONFIG_FILE" ]; then
|
|
35
|
-
# Fallback to user home directory (created by postinstall)
|
|
36
|
-
if [ -f "$HOME/.devicely/devices.conf" ]; then
|
|
37
|
-
CONFIG_FILE="$HOME/.devicely/devices.conf"
|
|
38
|
-
else
|
|
39
|
-
echo "❌ Required file not found: devices.conf"
|
|
40
|
-
echo " Searched in: script dir, config/, ~/.devicely/, global npm"
|
|
41
|
-
exit 1
|
|
42
|
-
fi
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
APPS_PRESETS_FILE=$(find_config "apps_presets.conf")
|
|
46
|
-
if [ -z "$APPS_PRESETS_FILE" ]; then
|
|
47
|
-
if [ -f "$HOME/.devicely/apps_presets.conf" ]; then
|
|
48
|
-
APPS_PRESETS_FILE="$HOME/.devicely/apps_presets.conf"
|
|
49
|
-
else
|
|
50
|
-
echo "❌ Required file not found: apps_presets.conf"
|
|
51
|
-
echo " Searched in: script dir, config/, ~/.devicely/, global npm"
|
|
52
|
-
exit 1
|
|
53
|
-
fi
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
# Session management (managed by backend server)
|
|
57
|
-
SCRIPT_DIR_ABS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
58
|
-
# Use OS temp directory for session file - works for both local and npm package
|
|
59
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
60
|
-
SESSION_MAP_FILE="$TMPDIR/devicely_android_sessions.map"
|
|
61
|
-
else
|
|
62
|
-
SESSION_MAP_FILE="/tmp/devicely_android_sessions.map"
|
|
63
|
-
fi
|
|
64
|
-
ELEMENT_CACHE_FILE="/tmp/android_elements_cache.$$"
|
|
65
|
-
DEVICE_PORTS_FILE="/tmp/android_multi_ports"
|
|
66
|
-
|
|
67
|
-
# Color codes for output
|
|
68
|
-
GREEN='\033[0;32m'
|
|
69
|
-
RED='\033[0;31m'
|
|
70
|
-
YELLOW='\033[1;33m'
|
|
71
|
-
BLUE='\033[0;34m'
|
|
72
|
-
NC='\033[0m' # No Color
|
|
73
|
-
|
|
74
|
-
# Cleanup on exit
|
|
75
|
-
cleanup() {
|
|
76
|
-
[ "$DEBUG" = "1" ] && echo -e "${YELLOW}Cleaning up Android connections...${NC}"
|
|
77
|
-
# Don't kill port forwards - keep UIAutomator2 server running for performance
|
|
78
|
-
# if [ -f "$DEVICE_PORTS_FILE" ]; then
|
|
79
|
-
# while IFS=: read -r serial port; do
|
|
80
|
-
# adb -s "$serial" forward --remove tcp:$port 2>/dev/null
|
|
81
|
-
# done < "$DEVICE_PORTS_FILE"
|
|
82
|
-
# fi
|
|
83
|
-
# Don't remove session map - reuse sessions for better performance
|
|
84
|
-
# rm -f "$SESSION_MAP_FILE" "$ELEMENT_CACHE_FILE" "$DEVICE_PORTS_FILE" 2>/dev/null
|
|
85
|
-
}
|
|
86
|
-
trap cleanup EXIT INT TERM
|
|
87
|
-
|
|
88
|
-
# Check if UIAutomator2 session is available (managed by backend server)
|
|
89
|
-
init_uiautomator2() {
|
|
90
|
-
local serial=$1
|
|
91
|
-
local port=${2:-8200} # Default port if not specified
|
|
92
|
-
|
|
93
|
-
# Check if device has active session from backend server
|
|
94
|
-
if [ -f "$SESSION_MAP_FILE" ]; then
|
|
95
|
-
local session_info=$(grep "^$serial:" "$SESSION_MAP_FILE" 2>/dev/null | tail -1)
|
|
96
|
-
if [ -n "$session_info" ]; then
|
|
97
|
-
# Format: udid:sessionId:port
|
|
98
|
-
# For IP:PORT format (100.87.246.132:5555:sessionId:port), we need the last field
|
|
99
|
-
local stored_port=$(echo "$session_info" | awk -F':' '{print $NF}')
|
|
100
|
-
|
|
101
|
-
# Verify port is numeric
|
|
102
|
-
if [[ "$stored_port" =~ ^[0-9]+$ ]]; then
|
|
103
|
-
echo -e "${GREEN}✓ Using UIAutomator2 session (port $stored_port)${NC}"
|
|
104
|
-
return 0
|
|
105
|
-
else
|
|
106
|
-
echo -e "${YELLOW}⚠️ Invalid session format in file${NC}"
|
|
107
|
-
fi
|
|
108
|
-
fi
|
|
109
|
-
fi
|
|
110
|
-
|
|
111
|
-
# No session found
|
|
112
|
-
echo -e "${RED}❌ No UIAutomator2 session found for $serial${NC}"
|
|
113
|
-
echo -e "${YELLOW}Please connect device first using the web UI 'Connect' button${NC}"
|
|
114
|
-
echo -e "${YELLOW}The UIAutomator2 session will be automatically started when you connect.${NC}"
|
|
115
|
-
return 1
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
# Get or create UIAutomator2 session
|
|
119
|
-
get_session() {
|
|
120
|
-
local serial=$1
|
|
121
|
-
local port=$2
|
|
122
|
-
|
|
123
|
-
# Check if session exists in persistent file (created by backend)
|
|
124
|
-
if [ -f "$SESSION_MAP_FILE" ]; then
|
|
125
|
-
local session_info=$(grep "^$serial:" "$SESSION_MAP_FILE" 2>/dev/null | tail -1)
|
|
126
|
-
if [ -n "$session_info" ]; then
|
|
127
|
-
# Format: udid:sessionId:port
|
|
128
|
-
# For IP:PORT udid (100.87.246.132:5555:sessionId:port), extract last 2 fields
|
|
129
|
-
local stored_port=$(echo "$session_info" | awk -F':' '{print $NF}')
|
|
130
|
-
local session_id=$(echo "$session_info" | awk -F':' '{print $(NF-1)}')
|
|
131
|
-
|
|
132
|
-
# Verify port is numeric
|
|
133
|
-
if [[ ! "$stored_port" =~ ^[0-9]+$ ]]; then
|
|
134
|
-
echo -e "${RED}❌ Invalid session format${NC}" >&2
|
|
135
|
-
return 1
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
# Use the port from the backend
|
|
139
|
-
port=$stored_port
|
|
140
|
-
|
|
141
|
-
# Verify session is still active
|
|
142
|
-
if curl -s "http://localhost:$port/wd/hub/session/$session_id/status" &>/dev/null 2>&1; then
|
|
143
|
-
echo "$session_id"
|
|
144
|
-
return 0
|
|
145
|
-
fi
|
|
146
|
-
fi
|
|
147
|
-
fi
|
|
148
|
-
|
|
149
|
-
# No persistent session found - need to connect device first
|
|
150
|
-
echo -e "${RED}No active session for $serial${NC}" >&2
|
|
151
|
-
echo -e "${YELLOW}Please connect device first using the web UI 'Connect' button${NC}" >&2
|
|
152
|
-
return 1
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
# Launch app
|
|
156
|
-
launch_app() {
|
|
157
|
-
local serial=$1
|
|
158
|
-
local port=$2
|
|
159
|
-
local package=$3
|
|
160
|
-
local activity=$4
|
|
161
|
-
|
|
162
|
-
echo -e "${BLUE}Launching $package...${NC}"
|
|
163
|
-
|
|
164
|
-
# Use ADB for faster launch
|
|
165
|
-
if [ -n "$activity" ]; then
|
|
166
|
-
adb -s "$serial" shell am start -n "$package/$activity"
|
|
167
|
-
else
|
|
168
|
-
adb -s "$serial" shell monkey -p "$package" -c android.intent.category.LAUNCHER 1
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
sleep 2
|
|
172
|
-
echo -e "${GREEN}✓ App launched${NC}"
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
# Open URL in default browser
|
|
176
|
-
open_url() {
|
|
177
|
-
local serial=$1
|
|
178
|
-
local url=$2
|
|
179
|
-
|
|
180
|
-
# Add https:// if not present
|
|
181
|
-
if ! echo "$url" | grep -qE '^[a-z]+://'; then
|
|
182
|
-
url="https://$url"
|
|
183
|
-
fi
|
|
184
|
-
|
|
185
|
-
echo -e "${BLUE}Opening URL: $url${NC}"
|
|
186
|
-
adb -s "$serial" shell am start -a android.intent.action.VIEW -d "$url"
|
|
187
|
-
sleep 2
|
|
188
|
-
echo -e "${GREEN}✓ URL opened${NC}"
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
# Close app
|
|
192
|
-
close_app() {
|
|
193
|
-
local serial=$1
|
|
194
|
-
local package=$2
|
|
195
|
-
|
|
196
|
-
echo -e "${BLUE}Closing $package...${NC}"
|
|
197
|
-
adb -s "$serial" shell am force-stop "$package"
|
|
198
|
-
echo -e "${GREEN}✓ App closed${NC}"
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
# Get UI hierarchy
|
|
202
|
-
get_ui_hierarchy() {
|
|
203
|
-
local serial=$1
|
|
204
|
-
local port=$2
|
|
205
|
-
local session_id=$3
|
|
206
|
-
|
|
207
|
-
# Get page source from UIAutomator2
|
|
208
|
-
curl -s "http://localhost:$port/wd/hub/session/$session_id/source" | python3 -c '
|
|
209
|
-
import sys, json
|
|
210
|
-
try:
|
|
211
|
-
data = json.load(sys.stdin)
|
|
212
|
-
print(data.get("value", ""))
|
|
213
|
-
except:
|
|
214
|
-
pass
|
|
215
|
-
'
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
# Get locators from current screen
|
|
219
|
-
get_locators() {
|
|
220
|
-
local serial=$1
|
|
221
|
-
|
|
222
|
-
[ "$DEBUG" = "1" ] && echo "🔍 Getting all locators from current screen..."
|
|
223
|
-
|
|
224
|
-
# Get port from session file
|
|
225
|
-
local port=""
|
|
226
|
-
if [ -f "$SESSION_MAP_FILE" ]; then
|
|
227
|
-
local session_info=$(grep "^$serial:" "$SESSION_MAP_FILE" 2>/dev/null | tail -1)
|
|
228
|
-
if [ -n "$session_info" ]; then
|
|
229
|
-
port=$(echo "$session_info" | awk -F':' '{print $NF}')
|
|
230
|
-
fi
|
|
231
|
-
fi
|
|
232
|
-
|
|
233
|
-
if [ -z "$port" ]; then
|
|
234
|
-
[ "$DEBUG" = "1" ] && echo -e "${RED}❌ No active session found${NC}"
|
|
235
|
-
return 1
|
|
236
|
-
fi
|
|
237
|
-
|
|
238
|
-
# Get existing session ID
|
|
239
|
-
local session_id=$(get_session "$serial" "$port")
|
|
240
|
-
|
|
241
|
-
if [ -z "$session_id" ] || [ "$session_id" == "None" ]; then
|
|
242
|
-
[ "$DEBUG" = "1" ] && echo -e "${RED}❌ No active UIAutomator2 session${NC}"
|
|
243
|
-
return 1
|
|
244
|
-
fi
|
|
245
|
-
|
|
246
|
-
[ "$DEBUG" = "1" ] && echo "📱 Using session: $session_id (port: $port)"
|
|
247
|
-
[ "$DEBUG" = "1" ] && echo "📱 Fetching page source..."
|
|
248
|
-
|
|
249
|
-
# Get page source
|
|
250
|
-
local page_source=$(curl -s "http://localhost:$port/wd/hub/session/$session_id/source" 2>/dev/null)
|
|
251
|
-
|
|
252
|
-
if [ -n "$page_source" ] && ! echo "$page_source" | grep -q '"error"'; then
|
|
253
|
-
[ "$DEBUG" = "1" ] && echo "🔍 Extracting locators..."
|
|
254
|
-
|
|
255
|
-
# Save page source to temp file for Python to read
|
|
256
|
-
local temp_source="/tmp/android_source_$$.json"
|
|
257
|
-
echo "$page_source" > "$temp_source"
|
|
258
|
-
|
|
259
|
-
# Extract all locator information using Python
|
|
260
|
-
python3 << PYTHON_SCRIPT
|
|
261
|
-
import sys
|
|
262
|
-
import json
|
|
263
|
-
import xml.etree.ElementTree as ET
|
|
264
|
-
|
|
265
|
-
try:
|
|
266
|
-
with open('$temp_source') as f:
|
|
267
|
-
data = json.load(f)
|
|
268
|
-
xml_str = data.get('value', '')
|
|
269
|
-
|
|
270
|
-
if not xml_str or len(xml_str) < 10:
|
|
271
|
-
print('\n❌ No page source found or empty XML')
|
|
272
|
-
print('💡 This may be a Camera/Video/Game app that uses native rendering')
|
|
273
|
-
print('💡 Try using coordinate-based tapping: tap X,Y')
|
|
274
|
-
sys.exit(1)
|
|
275
|
-
|
|
276
|
-
root = ET.fromstring(xml_str)
|
|
277
|
-
locators = []
|
|
278
|
-
|
|
279
|
-
# Extract locators from all elements
|
|
280
|
-
for elem in root.iter():
|
|
281
|
-
locator_info = {}
|
|
282
|
-
|
|
283
|
-
# Get element class
|
|
284
|
-
elem_class = elem.get('class', '')
|
|
285
|
-
if elem_class:
|
|
286
|
-
# Shorten Android class names
|
|
287
|
-
elem_class = elem_class.split('.')[-1]
|
|
288
|
-
locator_info['type'] = elem_class
|
|
289
|
-
|
|
290
|
-
# Get text, content-desc, resource-id
|
|
291
|
-
text = elem.get('text', '')
|
|
292
|
-
content_desc = elem.get('content-desc', '')
|
|
293
|
-
resource_id = elem.get('resource-id', '')
|
|
294
|
-
|
|
295
|
-
# Extract just the ID part from resource-id
|
|
296
|
-
if resource_id and ':id/' in resource_id:
|
|
297
|
-
resource_id = resource_id.split(':id/')[-1]
|
|
298
|
-
|
|
299
|
-
if text:
|
|
300
|
-
locator_info['text'] = text
|
|
301
|
-
if content_desc:
|
|
302
|
-
locator_info['content-desc'] = content_desc
|
|
303
|
-
if resource_id:
|
|
304
|
-
locator_info['resource-id'] = resource_id
|
|
305
|
-
|
|
306
|
-
# Get properties
|
|
307
|
-
clickable = elem.get('clickable', 'false')
|
|
308
|
-
enabled = elem.get('enabled', 'true')
|
|
309
|
-
focusable = elem.get('focusable', 'false')
|
|
310
|
-
|
|
311
|
-
locator_info['clickable'] = clickable
|
|
312
|
-
locator_info['enabled'] = enabled
|
|
313
|
-
|
|
314
|
-
# Get coordinates from bounds [x1,y1][x2,y2]
|
|
315
|
-
bounds = elem.get('bounds', '')
|
|
316
|
-
if bounds:
|
|
317
|
-
import re
|
|
318
|
-
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
|
|
319
|
-
if match:
|
|
320
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
321
|
-
center_x = (x1 + x2) // 2
|
|
322
|
-
center_y = (y1 + y2) // 2
|
|
323
|
-
width = x2 - x1
|
|
324
|
-
height = y2 - y1
|
|
325
|
-
locator_info['bounds'] = f'({center_x},{center_y}) {width}x{height}'
|
|
326
|
-
|
|
327
|
-
# Only add if we have meaningful locator information
|
|
328
|
-
if text or content_desc or resource_id:
|
|
329
|
-
locators.append(locator_info)
|
|
330
|
-
|
|
331
|
-
# Print formatted output - only in DEBUG mode
|
|
332
|
-
if locators:
|
|
333
|
-
import os
|
|
334
|
-
debug = os.environ.get('DEBUG', '0') == '1'
|
|
335
|
-
if debug:
|
|
336
|
-
print(f'\n📋 Found {len(locators)} interactive elements:\n', file=sys.stderr)
|
|
337
|
-
print('=' * 120, file=sys.stderr)
|
|
338
|
-
|
|
339
|
-
for i, loc in enumerate(locators, 1):
|
|
340
|
-
if debug:
|
|
341
|
-
# Build single line output with Name first
|
|
342
|
-
parts = []
|
|
343
|
-
|
|
344
|
-
# Start with number and primary identifier
|
|
345
|
-
text = loc.get('text', '')
|
|
346
|
-
desc = loc.get('content-desc', '')
|
|
347
|
-
res_id = loc.get('resource-id', '')
|
|
348
|
-
|
|
349
|
-
# Choose primary name (prefer text, then desc, then id)
|
|
350
|
-
primary_name = text or desc or res_id or '(no name)'
|
|
351
|
-
parts.append(f'{i:3d}. 📍 {primary_name:40s}')
|
|
352
|
-
|
|
353
|
-
# Add Type
|
|
354
|
-
elem_type = loc.get('type', 'Unknown')
|
|
355
|
-
parts.append(f'[{elem_type:15s}]')
|
|
356
|
-
|
|
357
|
-
# Add Label/Desc if different from primary name
|
|
358
|
-
if desc and desc != primary_name:
|
|
359
|
-
parts.append(f'Label:{desc[:30]}{"..." if len(desc) > 30 else ""}')
|
|
360
|
-
|
|
361
|
-
# Add Value/Text if different from primary
|
|
362
|
-
if text and text != primary_name and text != desc:
|
|
363
|
-
parts.append(f'Val:{text[:20]}{"..." if len(text) > 20 else ""}')
|
|
364
|
-
|
|
365
|
-
# Add resource ID if not primary
|
|
366
|
-
if res_id and res_id != primary_name:
|
|
367
|
-
parts.append(f'ID:{res_id}')
|
|
368
|
-
|
|
369
|
-
# Add Position
|
|
370
|
-
if loc.get('bounds'):
|
|
371
|
-
parts.append(f'@ {loc["bounds"]}')
|
|
372
|
-
|
|
373
|
-
# Add status flags
|
|
374
|
-
flags = []
|
|
375
|
-
if loc.get('enabled') == 'true':
|
|
376
|
-
flags.append('✓En')
|
|
377
|
-
if loc.get('clickable') == 'true':
|
|
378
|
-
flags.append('✓Click')
|
|
379
|
-
if flags:
|
|
380
|
-
parts.append(' '.join(flags))
|
|
381
|
-
|
|
382
|
-
print(' ' + ' │ '.join(parts), file=sys.stderr)
|
|
383
|
-
|
|
384
|
-
if debug:
|
|
385
|
-
print('=' * 120, file=sys.stderr)
|
|
386
|
-
|
|
387
|
-
# Output JSON to stdout (always, regardless of DEBUG)
|
|
388
|
-
import json
|
|
389
|
-
print(json.dumps(locators))
|
|
390
|
-
|
|
391
|
-
except ET.ParseError as e:
|
|
392
|
-
import os
|
|
393
|
-
debug = os.environ.get('DEBUG', '0') == '1'
|
|
394
|
-
if debug:
|
|
395
|
-
print(f'\n❌ Error parsing XML: {e}', file=sys.stderr)
|
|
396
|
-
print('💡 The page source may be corrupted or in unexpected format', file=sys.stderr)
|
|
397
|
-
sys.exit(1)
|
|
398
|
-
except Exception as e:
|
|
399
|
-
import os
|
|
400
|
-
debug = os.environ.get('DEBUG', '0') == '1'
|
|
401
|
-
if debug:
|
|
402
|
-
print(f'\n❌ Error extracting locators: {e}', file=sys.stderr)
|
|
403
|
-
sys.exit(1)
|
|
404
|
-
PYTHON_SCRIPT
|
|
405
|
-
|
|
406
|
-
local exit_code=$?
|
|
407
|
-
|
|
408
|
-
# Cleanup temp file only (keep session alive for reuse)
|
|
409
|
-
rm -f "$temp_source"
|
|
410
|
-
|
|
411
|
-
if [ $exit_code -eq 0 ]; then
|
|
412
|
-
[ "$DEBUG" = "1" ] && echo -e "${GREEN}✅ Locators extraction complete${NC}"
|
|
413
|
-
return 0
|
|
414
|
-
else
|
|
415
|
-
echo -e "${YELLOW}⚠️ Locators extraction completed with warnings${NC}"
|
|
416
|
-
return 0
|
|
417
|
-
fi
|
|
418
|
-
else
|
|
419
|
-
echo -e "${RED}❌ Failed to get page source from device${NC}"
|
|
420
|
-
echo -e "${YELLOW}💡 Possible reasons:${NC}"
|
|
421
|
-
echo -e " • Camera/Video apps don't expose UI to accessibility"
|
|
422
|
-
echo -e " • Screen may be locked"
|
|
423
|
-
echo -e " • UIAutomator2 server not responding"
|
|
424
|
-
echo ""
|
|
425
|
-
echo -e "${BLUE}💡 Try:${NC}"
|
|
426
|
-
echo -e " 1. screenshot - Take a screenshot"
|
|
427
|
-
echo -e " 2. tap X,Y - Use coordinate-based tapping"
|
|
428
|
-
echo -e " 3. home - Go to home screen first"
|
|
429
|
-
|
|
430
|
-
# Cleanup session
|
|
431
|
-
curl -s -X DELETE "http://localhost:$port/wd/hub/session/$session_id" > /dev/null 2>&1
|
|
432
|
-
return 1
|
|
433
|
-
fi
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
# Helper function to find element with UIAutomator2
|
|
437
|
-
find_element_uia2() {
|
|
438
|
-
local session_id=$1
|
|
439
|
-
local port=$2
|
|
440
|
-
local strategy=$3
|
|
441
|
-
local selector=$4
|
|
442
|
-
local temp_file="/tmp/uia2_find_$$.json"
|
|
443
|
-
|
|
444
|
-
# Use correct JSON keys based on strategy type
|
|
445
|
-
if [[ "$strategy" == "xpath" ]]; then
|
|
446
|
-
# XPath uses 'using' and 'value'
|
|
447
|
-
curl -s -X POST "http://localhost:$port/wd/hub/session/$session_id/element" \
|
|
448
|
-
-H "Content-Type: application/json" \
|
|
449
|
-
-d "{\"using\": \"$strategy\", \"value\": \"$selector\"}" \
|
|
450
|
-
> "$temp_file" 2>/dev/null
|
|
451
|
-
else
|
|
452
|
-
# UIAutomator uses 'strategy' and 'selector'
|
|
453
|
-
curl -s -X POST "http://localhost:$port/wd/hub/session/$session_id/element" \
|
|
454
|
-
-H "Content-Type: application/json" \
|
|
455
|
-
-d "{\"strategy\": \"$strategy\", \"selector\": \"$selector\"}" \
|
|
456
|
-
> "$temp_file" 2>/dev/null
|
|
457
|
-
fi
|
|
458
|
-
|
|
459
|
-
local element_id=$(python3 -c "
|
|
460
|
-
import json
|
|
461
|
-
try:
|
|
462
|
-
with open('$temp_file') as f:
|
|
463
|
-
data = json.load(f)
|
|
464
|
-
value = data.get('value', {})
|
|
465
|
-
elem = value.get('element-6066-11e4-a52e-4f735466cecf') or value.get('ELEMENT')
|
|
466
|
-
if elem:
|
|
467
|
-
print(elem)
|
|
468
|
-
except:
|
|
469
|
-
pass
|
|
470
|
-
" 2>/dev/null)
|
|
471
|
-
|
|
472
|
-
rm -f "$temp_file"
|
|
473
|
-
echo "$element_id"
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
# Click element by text or coordinates
|
|
477
|
-
click_element() {
|
|
478
|
-
local serial=$1
|
|
479
|
-
local port=$2
|
|
480
|
-
local target="$3"
|
|
481
|
-
|
|
482
|
-
# Strip surrounding quotes if present (from UI commands like click "VPN")
|
|
483
|
-
target="${target#\"}" # Remove leading "
|
|
484
|
-
target="${target%\"}" # Remove trailing "
|
|
485
|
-
target="${target#\'}" # Remove leading '
|
|
486
|
-
target="${target%\'}" # Remove trailing '
|
|
487
|
-
|
|
488
|
-
# Check if coordinates (x,y format)
|
|
489
|
-
if [[ "$target" =~ ^[0-9]+,[0-9]+$ ]]; then
|
|
490
|
-
local x=$(echo "$target" | cut -d',' -f1)
|
|
491
|
-
local y=$(echo "$target" | cut -d',' -f2)
|
|
492
|
-
|
|
493
|
-
echo -e "${BLUE}Clicking at coordinates ($x, $y) using ADB...${NC}"
|
|
494
|
-
adb -s "$serial" shell input tap $x $y
|
|
495
|
-
echo -e "${GREEN}✓ Click completed${NC}"
|
|
496
|
-
return 0
|
|
497
|
-
fi
|
|
498
|
-
|
|
499
|
-
# For text-based clicking, we need UIAutomator2
|
|
500
|
-
local session_id=$(get_session "$serial" "$port")
|
|
501
|
-
if [ -z "$session_id" ]; then
|
|
502
|
-
echo -e "${YELLOW}Tip: Use coordinates (x,y) to click without UIAutomator2${NC}"
|
|
503
|
-
return 1
|
|
504
|
-
fi
|
|
505
|
-
|
|
506
|
-
# Find element by text
|
|
507
|
-
echo -e "${BLUE}🔎 Accessibility search: '$target'${NC}"
|
|
508
|
-
|
|
509
|
-
# Try multiple strategies (like Appium Inspector)
|
|
510
|
-
local element_id=""
|
|
511
|
-
local response=""
|
|
512
|
-
|
|
513
|
-
# Strategy 1: Android UiAutomator text (MOST RELIABLE - what Appium uses)
|
|
514
|
-
echo -e "${BLUE} Strategy 1: UiAutomator text()...${NC}"
|
|
515
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().text(\\\"$target\\\")")
|
|
516
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.text() - Element: $element_id${NC}"
|
|
517
|
-
|
|
518
|
-
# Strategy 2: UiAutomator textContains (partial match)
|
|
519
|
-
if [ -z "$element_id" ]; then
|
|
520
|
-
echo -e "${BLUE} Strategy 2: UiAutomator textContains()...${NC}"
|
|
521
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().textContains(\\\"$target\\\")")
|
|
522
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.textContains()${NC}"
|
|
523
|
-
fi
|
|
524
|
-
|
|
525
|
-
# Strategy 3: UiAutomator description (content-desc)
|
|
526
|
-
if [ -z "$element_id" ]; then
|
|
527
|
-
echo -e "${BLUE} Strategy 3: UiAutomator description()...${NC}"
|
|
528
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().description(\\\"$target\\\")")
|
|
529
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.description()${NC}"
|
|
530
|
-
fi
|
|
531
|
-
|
|
532
|
-
# Strategy 4: UiAutomator descriptionContains
|
|
533
|
-
if [ -z "$element_id" ]; then
|
|
534
|
-
echo -e "${BLUE} Strategy 4: UiAutomator descriptionContains()...${NC}"
|
|
535
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().descriptionContains(\\\"$target\\\")")
|
|
536
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.descriptionContains()${NC}"
|
|
537
|
-
fi
|
|
538
|
-
|
|
539
|
-
# Strategy 5: UiAutomator resourceId
|
|
540
|
-
if [ -z "$element_id" ]; then
|
|
541
|
-
echo -e "${BLUE} Strategy 5: UiAutomator resourceId()...${NC}"
|
|
542
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "-android uiautomator" "new UiSelector().resourceIdMatches(\\\".*$target.*\\\")")
|
|
543
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by UiSelector.resourceId()${NC}"
|
|
544
|
-
fi
|
|
545
|
-
|
|
546
|
-
# Strategy 6: XPath exact text (fallback)
|
|
547
|
-
if [ -z "$element_id" ]; then
|
|
548
|
-
echo -e "${BLUE} Strategy 6: XPath exact text...${NC}"
|
|
549
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "xpath" "//*[@text='$target']")
|
|
550
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by XPath text${NC}"
|
|
551
|
-
fi
|
|
552
|
-
|
|
553
|
-
# Strategy 7: XPath content-desc
|
|
554
|
-
if [ -z "$element_id" ]; then
|
|
555
|
-
echo -e "${BLUE} Strategy 7: XPath content-desc...${NC}"
|
|
556
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "xpath" "//*[@content-desc='$target']")
|
|
557
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by XPath content-desc${NC}"
|
|
558
|
-
fi
|
|
559
|
-
|
|
560
|
-
# Strategy 8: XPath partial text
|
|
561
|
-
if [ -z "$element_id" ]; then
|
|
562
|
-
echo -e "${BLUE} Strategy 8: XPath partial text...${NC}"
|
|
563
|
-
element_id=$(find_element_uia2 "$session_id" "$port" "xpath" "//*[contains(@text, '$target')]")
|
|
564
|
-
[ -n "$element_id" ] && echo -e "${GREEN} ✓ Found by XPath partial text${NC}"
|
|
565
|
-
fi
|
|
566
|
-
|
|
567
|
-
if [ -z "$element_id" ]; then
|
|
568
|
-
echo -e "${RED}✗ Element not found: $target${NC}"
|
|
569
|
-
echo -e "${YELLOW}Tip: Use coordinates (x,y) to click specific locations${NC}"
|
|
570
|
-
return 1
|
|
571
|
-
fi
|
|
572
|
-
|
|
573
|
-
echo -e "${GREEN}✓ Element found, clicking...${NC}"
|
|
574
|
-
curl -s -X POST "http://localhost:$port/wd/hub/session/$session_id/element/$element_id/click" &>/dev/null
|
|
575
|
-
|
|
576
|
-
echo -e "${GREEN}✓ Click completed${NC}"
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
# Type text
|
|
580
|
-
type_text() {
|
|
581
|
-
local serial=$1
|
|
582
|
-
local text=$2
|
|
583
|
-
|
|
584
|
-
echo -e "${BLUE}Typing: $text${NC}"
|
|
585
|
-
|
|
586
|
-
# Use ADB directly for text input (no UIAutomator2 needed)
|
|
587
|
-
# Replace spaces with %s for ADB compatibility
|
|
588
|
-
local formatted_text=$(echo "$text" | sed 's/ /%s/g')
|
|
589
|
-
adb -s "$serial" shell input text "$formatted_text"
|
|
590
|
-
|
|
591
|
-
echo -e "${GREEN}✓ Text entered${NC}"
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
# Swipe gesture
|
|
595
|
-
swipe() {
|
|
596
|
-
local serial=$1
|
|
597
|
-
local direction=$2
|
|
598
|
-
|
|
599
|
-
echo -e "${BLUE}Swiping $direction...${NC}"
|
|
600
|
-
|
|
601
|
-
# Get screen size using ADB
|
|
602
|
-
local screen_size=$(adb -s "$serial" shell wm size | grep -o '[0-9]*x[0-9]*')
|
|
603
|
-
local width=$(echo "$screen_size" | cut -d'x' -f1)
|
|
604
|
-
local height=$(echo "$screen_size" | cut -d'x' -f2)
|
|
605
|
-
|
|
606
|
-
local x1=$((width / 2))
|
|
607
|
-
local y1=$((height / 2))
|
|
608
|
-
local x2=$x1
|
|
609
|
-
local y2=$y1
|
|
610
|
-
|
|
611
|
-
case "$direction" in
|
|
612
|
-
up)
|
|
613
|
-
y1=$((height * 3 / 4))
|
|
614
|
-
y2=$((height / 4))
|
|
615
|
-
;;
|
|
616
|
-
down)
|
|
617
|
-
y1=$((height / 4))
|
|
618
|
-
y2=$((height * 3 / 4))
|
|
619
|
-
;;
|
|
620
|
-
left)
|
|
621
|
-
x1=$((width * 3 / 4))
|
|
622
|
-
x2=$((width / 4))
|
|
623
|
-
;;
|
|
624
|
-
right)
|
|
625
|
-
x1=$((width / 4))
|
|
626
|
-
x2=$((width * 3 / 4))
|
|
627
|
-
;;
|
|
628
|
-
esac
|
|
629
|
-
|
|
630
|
-
adb -s "$serial" shell input swipe $x1 $y1 $x2 $y2 300
|
|
631
|
-
echo -e "${GREEN}✓ Swipe completed${NC}"
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
# Screenshot
|
|
635
|
-
take_screenshot() {
|
|
636
|
-
local serial=$1
|
|
637
|
-
# Save to webapp screenshots directory
|
|
638
|
-
local screenshots_dir="$SCRIPT_DIR/webapp/backend/screenshots"
|
|
639
|
-
mkdir -p "$screenshots_dir"
|
|
640
|
-
local output="${screenshots_dir}/screenshot_android_${serial}_$(date +%Y%m%d_%H%M%S).png"
|
|
641
|
-
|
|
642
|
-
echo -e "${BLUE}Taking screenshot...${NC}"
|
|
643
|
-
adb -s "$serial" exec-out screencap -p > "$output"
|
|
644
|
-
echo -e "${GREEN}✓ Screenshot saved: $output${NC}"
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
# Home button
|
|
648
|
-
press_home() {
|
|
649
|
-
local serial=$1
|
|
650
|
-
echo -e "${BLUE}Pressing Home button...${NC}"
|
|
651
|
-
adb -s "$serial" shell input keyevent KEYCODE_HOME
|
|
652
|
-
echo -e "${GREEN}✓ Home pressed${NC}"
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
# Back button
|
|
656
|
-
press_back() {
|
|
657
|
-
local serial=$1
|
|
658
|
-
echo -e "${BLUE}Pressing Back button...${NC}"
|
|
659
|
-
adb -s "$serial" shell input keyevent KEYCODE_BACK
|
|
660
|
-
echo -e "${GREEN}✓ Back pressed${NC}"
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
# Lock device
|
|
664
|
-
lock_device() {
|
|
665
|
-
local serial=$1
|
|
666
|
-
echo -e "${BLUE}Locking device...${NC}"
|
|
667
|
-
adb -s "$serial" shell input keyevent KEYCODE_POWER
|
|
668
|
-
echo -e "${GREEN}✓ Device locked${NC}"
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
# Unlock device
|
|
672
|
-
unlock_device() {
|
|
673
|
-
local serial=$1
|
|
674
|
-
echo -e "${BLUE}Unlocking device...${NC}"
|
|
675
|
-
adb -s "$serial" shell input keyevent KEYCODE_MENU
|
|
676
|
-
echo -e "${GREEN}✓ Device unlocked (if screen on)${NC}"
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
# Restart device
|
|
680
|
-
restart_device() {
|
|
681
|
-
local serial=$1
|
|
682
|
-
echo -e "${BLUE}Restarting device...${NC}"
|
|
683
|
-
adb -s "$serial" reboot
|
|
684
|
-
echo -e "${GREEN}✓ Restart command sent${NC}"
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
# Shutdown device
|
|
688
|
-
shutdown_device() {
|
|
689
|
-
local serial=$1
|
|
690
|
-
echo -e "${BLUE}Shutting down device...${NC}"
|
|
691
|
-
adb -s "$serial" shell reboot -p
|
|
692
|
-
echo -e "${GREEN}✓ Shutdown command sent${NC}"
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
# Get device info
|
|
696
|
-
get_device_info() {
|
|
697
|
-
local serial=$1
|
|
698
|
-
|
|
699
|
-
echo -e "${BLUE}Device Information:${NC}"
|
|
700
|
-
echo "Serial: $serial"
|
|
701
|
-
echo "Model: $(adb -s "$serial" shell getprop ro.product.model | tr -d '\r')"
|
|
702
|
-
echo "Android: $(adb -s "$serial" shell getprop ro.build.version.release | tr -d '\r')"
|
|
703
|
-
echo "SDK: $(adb -s "$serial" shell getprop ro.build.version.sdk | tr -d '\r')"
|
|
704
|
-
echo "Battery: $(adb -s "$serial" shell dumpsys battery | grep level | cut -d':' -f2 | tr -d ' \r')%"
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
# List installed apps
|
|
708
|
-
list_apps() {
|
|
709
|
-
local serial=$1
|
|
710
|
-
echo -e "${BLUE}Installed Apps:${NC}"
|
|
711
|
-
adb -s "$serial" shell pm list packages -3 | sed 's/package://' | sort
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
# Main command processor
|
|
715
|
-
process_command() {
|
|
716
|
-
local serial=$1
|
|
717
|
-
local port=$2
|
|
718
|
-
local command=$3
|
|
719
|
-
shift 3
|
|
720
|
-
local args="$@"
|
|
721
|
-
|
|
722
|
-
case "$command" in
|
|
723
|
-
launch)
|
|
724
|
-
launch_app "$serial" "$port" "$args"
|
|
725
|
-
;;
|
|
726
|
-
close|kill)
|
|
727
|
-
close_app "$serial" "$args"
|
|
728
|
-
;;
|
|
729
|
-
url)
|
|
730
|
-
open_url "$serial" "$args"
|
|
731
|
-
;;
|
|
732
|
-
click|tap)
|
|
733
|
-
click_element "$serial" "$port" "$args"
|
|
734
|
-
;;
|
|
735
|
-
type)
|
|
736
|
-
# Use ADB directly, no port needed
|
|
737
|
-
type_text "$serial" "$args"
|
|
738
|
-
;;
|
|
739
|
-
swipe)
|
|
740
|
-
# Use ADB directly, no port needed
|
|
741
|
-
swipe "$serial" "$args"
|
|
742
|
-
;;
|
|
743
|
-
screenshot)
|
|
744
|
-
take_screenshot "$serial" "$args"
|
|
745
|
-
;;
|
|
746
|
-
getLocators)
|
|
747
|
-
get_locators "$serial"
|
|
748
|
-
;;
|
|
749
|
-
home)
|
|
750
|
-
press_home "$serial"
|
|
751
|
-
;;
|
|
752
|
-
back)
|
|
753
|
-
press_back "$serial"
|
|
754
|
-
;;
|
|
755
|
-
lock)
|
|
756
|
-
lock_device "$serial"
|
|
757
|
-
;;
|
|
758
|
-
unlock)
|
|
759
|
-
unlock_device "$serial"
|
|
760
|
-
;;
|
|
761
|
-
restart|reboot)
|
|
762
|
-
restart_device "$serial"
|
|
763
|
-
;;
|
|
764
|
-
shutdown|poweroff)
|
|
765
|
-
shutdown_device "$serial"
|
|
766
|
-
;;
|
|
767
|
-
info)
|
|
768
|
-
get_device_info "$serial"
|
|
769
|
-
;;
|
|
770
|
-
listapps)
|
|
771
|
-
list_apps "$serial"
|
|
772
|
-
;;
|
|
773
|
-
*)
|
|
774
|
-
echo -e "${RED}Unknown command: $command${NC}"
|
|
775
|
-
return 1
|
|
776
|
-
;;
|
|
777
|
-
esac
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
# Main execution
|
|
781
|
-
main() {
|
|
782
|
-
if [ $# -lt 2 ]; then
|
|
783
|
-
echo "Usage: $0 <device_serial> <command> [args...]"
|
|
784
|
-
echo ""
|
|
785
|
-
echo "Commands:"
|
|
786
|
-
echo " launch <package> [activity] - Launch app"
|
|
787
|
-
echo " close <package> - Close app"
|
|
788
|
-
echo " url <url> - Open URL in browser"
|
|
789
|
-
echo " click <text|x,y> - Click element"
|
|
790
|
-
echo " type <text> - Type text"
|
|
791
|
-
echo " swipe <up|down|left|right> - Swipe gesture"
|
|
792
|
-
echo " screenshot [filename] - Take screenshot"
|
|
793
|
-
echo " getLocators - Get UI elements"
|
|
794
|
-
echo " home - Press home button"
|
|
795
|
-
echo " back - Press back button"
|
|
796
|
-
echo " lock - Lock device"
|
|
797
|
-
echo " unlock - Unlock device"
|
|
798
|
-
echo " restart - Restart device"
|
|
799
|
-
echo " shutdown - Shutdown device"
|
|
800
|
-
echo " info - Device information"
|
|
801
|
-
echo " listapps - List installed apps"
|
|
802
|
-
exit 1
|
|
803
|
-
fi
|
|
804
|
-
|
|
805
|
-
local serial=$1
|
|
806
|
-
shift
|
|
807
|
-
|
|
808
|
-
# Verify device is connected
|
|
809
|
-
if ! adb devices | grep -q "$serial"; then
|
|
810
|
-
echo -e "${RED}Device not found: $serial${NC}"
|
|
811
|
-
echo "Available devices:"
|
|
812
|
-
adb devices
|
|
813
|
-
exit 1
|
|
814
|
-
fi
|
|
815
|
-
|
|
816
|
-
# Assign port and store mapping
|
|
817
|
-
local port=$((UIAUTOMATOR2_PORT_BASE))
|
|
818
|
-
echo "$serial:$port" >> "$DEVICE_PORTS_FILE"
|
|
819
|
-
|
|
820
|
-
# Initialize UIAutomator2 ONLY if needed (for commands that require it)
|
|
821
|
-
local command=$1
|
|
822
|
-
local requires_uiautomator=false
|
|
823
|
-
|
|
824
|
-
case "$command" in
|
|
825
|
-
click|tap|getLocators)
|
|
826
|
-
# Only require UIAutomator2 if NOT using coordinates
|
|
827
|
-
if [[ "$command" == "getLocators" ]] || ! [[ "$2" =~ ^[0-9]+,[0-9]+$ ]]; then
|
|
828
|
-
requires_uiautomator=true
|
|
829
|
-
fi
|
|
830
|
-
;;
|
|
831
|
-
esac
|
|
832
|
-
|
|
833
|
-
if [ "$requires_uiautomator" = true ]; then
|
|
834
|
-
echo -e "${YELLOW}Note: This command requires UIAutomator2${NC}"
|
|
835
|
-
if ! init_uiautomator2 "$serial" "$port"; then
|
|
836
|
-
echo -e "${YELLOW}Tip: Most commands (type, swipe, home, back, launch, screenshot) work without UIAutomator2${NC}"
|
|
837
|
-
exit 1
|
|
838
|
-
fi
|
|
839
|
-
fi
|
|
840
|
-
|
|
841
|
-
# Process command
|
|
842
|
-
process_command "$serial" "$port" "$@"
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
# Run if not sourced
|
|
846
|
-
if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
|
|
847
|
-
main "$@"
|
|
848
|
-
fi
|