device-devtools-mcp 0.1.0
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/AGENTS.md +327 -0
- package/LICENSE +21 -0
- package/README.md +491 -0
- package/VERSION +1 -0
- package/bin/device-devtools-mcp.js +106 -0
- package/bin/devicetools +315 -0
- package/config.example.json +63 -0
- package/integrations/agent-pointer.sh +48 -0
- package/integrations/claude/SKILL.md +8 -0
- package/integrations/cursor/devicetools.mdc +8 -0
- package/integrations/gemini/GEMINI.md +5 -0
- package/integrations/mcp/README.md +184 -0
- package/integrations/mcp/mcp.json +10 -0
- package/integrations/mcp/reference.sh +119 -0
- package/integrations/mcp/selftest.sh +158 -0
- package/integrations/mcp/server.sh +343 -0
- package/package.json +50 -0
- package/scripts/android/app.sh +241 -0
- package/scripts/android/back.sh +73 -0
- package/scripts/android/controls.sh +56 -0
- package/scripts/android/devices.sh +77 -0
- package/scripts/android/doctor.sh +253 -0
- package/scripts/android/lib.sh +699 -0
- package/scripts/android/logs.sh +289 -0
- package/scripts/android/permission.sh +119 -0
- package/scripts/android/settings.sh +63 -0
- package/scripts/android/setup.sh +97 -0
- package/scripts/android/tree.awk +166 -0
- package/scripts/android/tree.sh +127 -0
- package/scripts/android/type.sh +191 -0
- package/scripts/common/find.sh +95 -0
- package/scripts/common/key.sh +65 -0
- package/scripts/common/lib.sh +14 -0
- package/scripts/common/measure.sh +170 -0
- package/scripts/common/open.sh +131 -0
- package/scripts/common/screenshot.sh +102 -0
- package/scripts/common/scroll.sh +177 -0
- package/scripts/common/snapshot.sh +69 -0
- package/scripts/common/swipe.sh +171 -0
- package/scripts/common/tap.sh +226 -0
- package/scripts/common/wait.sh +212 -0
- package/scripts/common/waypoint.sh +141 -0
- package/scripts/dispatch.sh +21 -0
- package/scripts/flow.sh +266 -0
- package/scripts/init.sh +101 -0
- package/scripts/ios/app.sh +404 -0
- package/scripts/ios/back.sh +95 -0
- package/scripts/ios/controls.sh +68 -0
- package/scripts/ios/devices.sh +80 -0
- package/scripts/ios/doctor.sh +386 -0
- package/scripts/ios/lib.sh +864 -0
- package/scripts/ios/logs.sh +272 -0
- package/scripts/ios/permission.sh +108 -0
- package/scripts/ios/settings.sh +76 -0
- package/scripts/ios/setup.sh +175 -0
- package/scripts/ios/tree.sh +128 -0
- package/scripts/ios/type.sh +178 -0
- package/scripts/lib.sh +1032 -0
- package/scripts/links.tsv +44 -0
- package/scripts/relink.sh +121 -0
- package/scripts/run.sh +415 -0
- package/scripts/selftest.sh +1709 -0
- package/scripts/snapshot.awk +362 -0
- package/scripts/verify-npm-package.js +133 -0
- package/tests/fixtures/ios-contacts-list.expected +52 -0
- package/tests/fixtures/ios-contacts-list.rows +140 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# logs.sh — device logs, collected in the background and read on demand.
|
|
3
|
+
#
|
|
4
|
+
# logs.sh start begin collecting
|
|
5
|
+
# logs.sh stop stop collecting
|
|
6
|
+
# logs.sh status is it collecting, and since when
|
|
7
|
+
# logs.sh [--since Ns] [--process P] [--grep S] [--lines N]
|
|
8
|
+
# read what has been collected
|
|
9
|
+
# --raw every line the device logged, not just
|
|
10
|
+
# the app's. The default keeps only the
|
|
11
|
+
# app's own output: the OS logs a great
|
|
12
|
+
# deal on an app's behalf under its
|
|
13
|
+
# process name, and none of that is the
|
|
14
|
+
# app talking. Use this when the default
|
|
15
|
+
# is empty and you need to know why.
|
|
16
|
+
# --network only lines that look like HTTP requests
|
|
17
|
+
# or responses, from the app under test.
|
|
18
|
+
# THIS IS NOT A PROXY: it filters what the
|
|
19
|
+
# app itself chose to print, so it shows no
|
|
20
|
+
# bodies, no headers the app did not log,
|
|
21
|
+
# and nothing at all from an app that logs
|
|
22
|
+
# none of it. In exchange it needs no CA
|
|
23
|
+
# certificate on the device and certificate
|
|
24
|
+
# pinning cannot block it.
|
|
25
|
+
# The regex is logs.network_pattern.
|
|
26
|
+
#
|
|
27
|
+
# WHY A COLLECTOR RATHER THAN A QUERY
|
|
28
|
+
#
|
|
29
|
+
# `idevicesyslog` is a live stream, not a searchable store: there is no way to
|
|
30
|
+
# ask it what happened thirty seconds ago. So collection has to be running
|
|
31
|
+
# before the thing you want to read about happens. Reading without a collector
|
|
32
|
+
# would quietly return nothing, which reads exactly like "there were no logs" —
|
|
33
|
+
# so instead it starts one and says the history begins here.
|
|
34
|
+
#
|
|
35
|
+
# NO --subsystem
|
|
36
|
+
#
|
|
37
|
+
# `--subsystem` is an os_log concept. On a pre-iOS-17 device reached through
|
|
38
|
+
# idevicesyslog the subsystem is not carried, so the flag would be a filter that
|
|
39
|
+
# silently matched nothing. --process and --grep are what actually exist.
|
|
40
|
+
#
|
|
41
|
+
# exit 0 lines printed, or a lifecycle verb succeeded
|
|
42
|
+
# exit 1 usage error
|
|
43
|
+
# exit 2 environment problem
|
|
44
|
+
# exit 4 nothing was being collected, or nothing matched
|
|
45
|
+
|
|
46
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
47
|
+
|
|
48
|
+
SINCE=""
|
|
49
|
+
PROCESS=""
|
|
50
|
+
GREP=""
|
|
51
|
+
NETWORK=0
|
|
52
|
+
RAW=0
|
|
53
|
+
LINES=""
|
|
54
|
+
VERB="read"
|
|
55
|
+
|
|
56
|
+
while [ $# -gt 0 ]; do
|
|
57
|
+
case "$1" in
|
|
58
|
+
start|stop|status) VERB="$1"; shift ;;
|
|
59
|
+
--since)
|
|
60
|
+
[ $# -ge 2 ] || die "--since needs a number of seconds" 1
|
|
61
|
+
case "${2%s}" in (*[!0-9]*|"") die "--since needs seconds, e.g. --since 30 or --since 30s, got: $2" 1 ;; esac
|
|
62
|
+
SINCE="${2%s}"; shift 2 ;;
|
|
63
|
+
--process)
|
|
64
|
+
[ $# -ge 2 ] || die "--process needs a name" 1
|
|
65
|
+
PROCESS="$2"; shift 2 ;;
|
|
66
|
+
--network) NETWORK=1; shift ;;
|
|
67
|
+
--raw) RAW=1; shift ;;
|
|
68
|
+
--grep)
|
|
69
|
+
[ $# -ge 2 ] || die "--grep needs a string" 1
|
|
70
|
+
GREP="$2"; shift 2 ;;
|
|
71
|
+
--lines)
|
|
72
|
+
[ $# -ge 2 ] || die "--lines needs a number" 1
|
|
73
|
+
case "$2" in (*[!0-9]*|"") die "--lines needs a positive integer, got: $2" 1 ;; esac
|
|
74
|
+
LINES="$2"; shift 2 ;;
|
|
75
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
76
|
+
*) die "unknown argument: $1 (usage: logs.sh [start|stop|status] [--since Ns] [--process P] [--grep S] [--lines N])" 1 ;;
|
|
77
|
+
esac
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
load_config
|
|
81
|
+
[ "$(ios_kind)" = simulator ] || need_cmd idevicesyslog
|
|
82
|
+
|
|
83
|
+
SD="$(state_dir)"
|
|
84
|
+
PIDFILE="$SD/syslog.pid"
|
|
85
|
+
LOGFILE="$SD/syslog.log"
|
|
86
|
+
|
|
87
|
+
collector_pid() {
|
|
88
|
+
[ -f "$PIDFILE" ] || return 1
|
|
89
|
+
local p
|
|
90
|
+
p="$(cat "$PIDFILE" 2>/dev/null || true)"
|
|
91
|
+
[ -n "$p" ] || return 1
|
|
92
|
+
kill -0 "$p" 2>/dev/null || return 1
|
|
93
|
+
printf '%s' "$p"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
start_collector() {
|
|
97
|
+
local udid max source
|
|
98
|
+
udid="$(device_udid)"
|
|
99
|
+
max="$(cfg '.logs.max_lines_kept' '200000')"
|
|
100
|
+
|
|
101
|
+
# Different producer, same contract: one line per log record on stdout. The
|
|
102
|
+
# stamping and rotation below do not care which one is upstream.
|
|
103
|
+
if [ "$(ios_kind)" = simulator ]; then
|
|
104
|
+
source="xcrun simctl spawn '$udid' log stream --style syslog --level debug"
|
|
105
|
+
else
|
|
106
|
+
source="idevicesyslog -u '$udid'"
|
|
107
|
+
fi
|
|
108
|
+
# Two things happen in this writer, and both have to happen in the writer
|
|
109
|
+
# rather than in the reader.
|
|
110
|
+
#
|
|
111
|
+
# Each line is prefixed with the epoch second it arrived: idevicesyslog's own
|
|
112
|
+
# timestamps carry no year and no timezone, which makes "the last N seconds"
|
|
113
|
+
# unanswerable across midnight.
|
|
114
|
+
#
|
|
115
|
+
# And the file is rotated at max lines. The device produced 21,000 lines in
|
|
116
|
+
# thirty seconds during testing, so an uncapped collector left running fills
|
|
117
|
+
# the disk. Rotation belongs here because a reader that truncates a file an
|
|
118
|
+
# open writer is appending to leaves the writer's offset past the new end,
|
|
119
|
+
# and every later line lands after a gap of NULs.
|
|
120
|
+
nohup sh -c "$source 2>/dev/null | perl -e '
|
|
121
|
+
\$| = 1;
|
|
122
|
+
my (\$f, \$max) = @ARGV;
|
|
123
|
+
my \$n = 0;
|
|
124
|
+
open(my \$fh, q{>>}, \$f) or die;
|
|
125
|
+
\$fh->autoflush(1);
|
|
126
|
+
while (my \$l = <STDIN>) {
|
|
127
|
+
print \$fh time() . qq{\t} . \$l;
|
|
128
|
+
if (++\$n >= \$max) { close \$fh; rename(\$f, \$f . q{.1}); open(\$fh, q{>}, \$f) or die; \$fh->autoflush(1); \$n = 0; }
|
|
129
|
+
}' '$LOGFILE' '$max'" \
|
|
130
|
+
>/dev/null 2>&1 &
|
|
131
|
+
printf '%s' "$!" > "$PIDFILE"
|
|
132
|
+
for _ in $(seq 1 20); do
|
|
133
|
+
collector_pid >/dev/null 2>&1 && break
|
|
134
|
+
sleep 0.2
|
|
135
|
+
done
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "$VERB" in
|
|
139
|
+
start)
|
|
140
|
+
if pid="$(collector_pid)"; then
|
|
141
|
+
printf 'OK logs — already collecting (pid %s, %s lines)\n' "$pid" "$(wc -l < "$LOGFILE" 2>/dev/null | tr -d ' ' || echo 0)"
|
|
142
|
+
exit 0
|
|
143
|
+
fi
|
|
144
|
+
start_collector
|
|
145
|
+
pid="$(collector_pid)" || die "could not start idevicesyslog — see $LOGFILE" 2
|
|
146
|
+
printf 'OK logs — collecting (pid %s) into %s\n' "$pid" "$LOGFILE"
|
|
147
|
+
exit 0
|
|
148
|
+
;;
|
|
149
|
+
|
|
150
|
+
stop)
|
|
151
|
+
if ! pid="$(collector_pid)"; then
|
|
152
|
+
printf 'OK logs — not collecting\n'
|
|
153
|
+
exit 0
|
|
154
|
+
fi
|
|
155
|
+
# The collector is a shell wrapping a pipeline, so the children have to go
|
|
156
|
+
# too or idevicesyslog keeps the USB channel open.
|
|
157
|
+
pkill -P "$pid" 2>/dev/null || true
|
|
158
|
+
kill "$pid" 2>/dev/null || true
|
|
159
|
+
rm -f "$PIDFILE"
|
|
160
|
+
printf 'OK logs — stopped (%s lines kept in %s)\n' "$(wc -l < "$LOGFILE" 2>/dev/null | tr -d ' ' || echo 0)" "$LOGFILE"
|
|
161
|
+
exit 0
|
|
162
|
+
;;
|
|
163
|
+
|
|
164
|
+
status)
|
|
165
|
+
if pid="$(collector_pid)"; then
|
|
166
|
+
printf 'OK logs — collecting (pid %s, %s lines) since %s\n' \
|
|
167
|
+
"$pid" "$(wc -l < "$LOGFILE" 2>/dev/null | tr -d ' ' || echo 0)" \
|
|
168
|
+
"$(head -1 "$LOGFILE" 2>/dev/null | cut -f1 | xargs -I{} date -r {} '+%H:%M:%S' 2>/dev/null || echo 'unknown')"
|
|
169
|
+
else
|
|
170
|
+
printf 'OK logs — not collecting\n'
|
|
171
|
+
fi
|
|
172
|
+
exit 0
|
|
173
|
+
;;
|
|
174
|
+
esac
|
|
175
|
+
|
|
176
|
+
# --- read ---------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
if ! collector_pid >/dev/null 2>&1; then
|
|
179
|
+
start_collector
|
|
180
|
+
collector_pid >/dev/null 2>&1 \
|
|
181
|
+
|| die "log collection was not running and could not be started — see $LOGFILE" 2
|
|
182
|
+
die "log collection was not running; it is now — history begins here, so repeat the action and read again" 4
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
[ -s "$LOGFILE" ] || [ -s "$LOGFILE.1" ] \
|
|
186
|
+
|| die "nothing collected yet — the device has not logged since collection started" 4
|
|
187
|
+
|
|
188
|
+
CUTOFF=0
|
|
189
|
+
[ -n "$SINCE" ] && CUTOFF="$(( $(date +%s) - SINCE ))"
|
|
190
|
+
[ -n "$LINES" ] || LINES="$(cfg '.logs.max_lines' '50')"
|
|
191
|
+
|
|
192
|
+
# The rotated half first, so the result stays in chronological order.
|
|
193
|
+
ROTATED=""
|
|
194
|
+
[ -s "$LOGFILE.1" ] && ROTATED="$LOGFILE.1"
|
|
195
|
+
out="$(awk -F'\t' -v cutoff="$CUTOFF" '$1 >= cutoff { sub(/^[0-9]+\t/, ""); print }' $ROTATED "$LOGFILE")"
|
|
196
|
+
|
|
197
|
+
# THE APP'S OWN LINES BY DEFAULT.
|
|
198
|
+
#
|
|
199
|
+
# The whole device syslog is thirty-five thousand lines of other people's
|
|
200
|
+
# processes, and even the lines carrying this app's process name are mostly the
|
|
201
|
+
# OS logging on its behalf. --raw is the way back to all of it, and --process
|
|
202
|
+
# says which other process to look at instead.
|
|
203
|
+
UNFILTERED="$out"
|
|
204
|
+
if [ -n "$PROCESS" ]; then
|
|
205
|
+
out="$(printf '%s\n' "$out" | grep -F -- "$PROCESS" || true)"
|
|
206
|
+
elif [ "$RAW" -eq 0 ]; then
|
|
207
|
+
out="$(printf '%s\n' "$out" | log_filter)"
|
|
208
|
+
fi
|
|
209
|
+
if [ -n "$GREP" ]; then
|
|
210
|
+
out="$(printf '%s\n' "$out" | grep -F -i -- "$GREP" || true)"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
if [ "$NETWORK" -eq 1 ]; then
|
|
214
|
+
# THIS IS NOT A PROXY, AND IT CANNOT BE ONE HERE. A proxy needs its CA
|
|
215
|
+
# certificate installed on the device, and the app under test pins its
|
|
216
|
+
# certificates — it would see a handshake failure and nothing else. So this
|
|
217
|
+
# filters the app's own log output: what the app chose to print, and nothing
|
|
218
|
+
# more. No bodies, no headers the app did not log, and nothing at all from an
|
|
219
|
+
# app that logs nothing.
|
|
220
|
+
#
|
|
221
|
+
# THE PATTERN IS STRICTER THAN IT LOOKS, AND THE DEVICE IS WHY. A plain
|
|
222
|
+
# /GET|POST|.../ matched bluetoothd on the phone this was built against,
|
|
223
|
+
# because BD_VSC_OLYMPIC_GET_EXT_ADV_DROPPED contains GET. So a verb has to
|
|
224
|
+
# sit on a word boundary AND be followed by something that looks like a URL.
|
|
225
|
+
# "forgetting the cache" was never a GET either.
|
|
226
|
+
# The Network panel means the app's traffic. Without this the filter searches
|
|
227
|
+
# the whole device syslog, which on this phone is thirty-five thousand lines
|
|
228
|
+
# of other people's processes.
|
|
229
|
+
# Scoping to the app already happened above, unless --raw asked for
|
|
230
|
+
# everything, in which case the caller said what they wanted.
|
|
231
|
+
PAT="$(cfg '.logs.network_pattern' '(^|[^A-Za-z0-9_])(GET|POST|PUT|PATCH|DELETE)[[:space:]]+(https?://|/)|HTTP/[0-9]')"
|
|
232
|
+
out="$(printf '%s\n' "$out" | awk -v pat="$PAT" '$0 ~ pat' || true)"
|
|
233
|
+
if [ -z "$out" ]; then
|
|
234
|
+
die "no log lines matched /$PAT/ — this reads the app's own logging rather than a proxy, so an app that does not print its requests shows nothing here. Set logs.network_pattern in the config to match how yours logs them" 4
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# A blank string still prints one empty line, which would read as a result.
|
|
239
|
+
out="$(printf '%s\n' "$out" | sed '/^[[:space:]]*$/d')"
|
|
240
|
+
|
|
241
|
+
if [ -z "$out" ]; then
|
|
242
|
+
filters=""
|
|
243
|
+
[ -n "$SINCE" ] && filters="$filters --since ${SINCE}s"
|
|
244
|
+
[ -n "$PROCESS" ] && filters="$filters --process $PROCESS"
|
|
245
|
+
[ -n "$GREP" ] && filters="$filters --grep $GREP"
|
|
246
|
+
|
|
247
|
+
# "Nothing" and "nothing of yours" are different answers, and the second one
|
|
248
|
+
# is the common one: an app that never calls os_log still has the OS logging
|
|
249
|
+
# on its behalf, and saying how many of those there were is the difference
|
|
250
|
+
# between "the log is broken" and "this app does not log".
|
|
251
|
+
extra=""
|
|
252
|
+
if [ "$RAW" -eq 0 ] && [ -z "$PROCESS" ]; then
|
|
253
|
+
app="$(cfg '.app.bundle_id' '')"; app="${app##*.}"
|
|
254
|
+
if [ -n "$app" ]; then
|
|
255
|
+
mine="$(printf '%s\n' "$UNFILTERED" \
|
|
256
|
+
| grep -icE "^[A-Za-z]{3} +[0-9]+ +[0-9:.]+ +${app}[([]" || true)"
|
|
257
|
+
[ "${mine:-0}" -gt 0 ] \
|
|
258
|
+
&& extra=" — $mine line(s) carry this app's process name, all of them from system frameworks; --raw shows them"
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
die "no log lines from the app under test${filters:- }${extra} — $(wc -l < "$LOGFILE" | tr -d ' ') lines collected in total" 4
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
n="$(printf '%s\n' "$out" | wc -l | tr -d ' ')"
|
|
265
|
+
if [ "$n" -le "$LINES" ]; then
|
|
266
|
+
printf '%s\n' "$out"
|
|
267
|
+
else
|
|
268
|
+
# The tail, not the head: when something has just gone wrong, the most recent
|
|
269
|
+
# lines are the ones that say why.
|
|
270
|
+
printf '%s\n' "$out" | tail -n "$LINES"
|
|
271
|
+
printf '… %s earlier lines dropped (--lines N)\n' "$(( n - LINES ))"
|
|
272
|
+
fi
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# permission.sh — grant or deny a privacy permission before the app asks.
|
|
3
|
+
#
|
|
4
|
+
# permission.sh <name>=<allow|deny> [<name>=<allow|deny> ...]
|
|
5
|
+
#
|
|
6
|
+
# permission.sh location=allow contacts=deny
|
|
7
|
+
#
|
|
8
|
+
# WHY THIS IS A STEP AND NOT AUTOMATIC
|
|
9
|
+
#
|
|
10
|
+
# Other tools dismiss permission dialogs for you. That hides whether the app
|
|
11
|
+
# asked at all — and for a production app, whether it asks, and for what, is itself
|
|
12
|
+
# under test. A prompt that quietly stops appearing is a real regression, and a
|
|
13
|
+
# tool that taps it away can never report one.
|
|
14
|
+
#
|
|
15
|
+
# TWO LIMITS THIS PLATFORM HAS AND ANDROID DOES NOT
|
|
16
|
+
#
|
|
17
|
+
# A physical iPhone has no interface for setting privacy permissions from the
|
|
18
|
+
# host. None: not in WebDriverAgent, not in Apple's own device tooling. So on a
|
|
19
|
+
# device this fails and says so, rather than reporting a success it did not
|
|
20
|
+
# achieve. Drive the dialog instead:
|
|
21
|
+
#
|
|
22
|
+
# - tap: { label: Cho phép, optional: true }
|
|
23
|
+
#
|
|
24
|
+
# And even on a simulator, Apple's list has no camera service. `camera=allow` is
|
|
25
|
+
# therefore refused rather than quietly skipped.
|
|
26
|
+
#
|
|
27
|
+
# exit 0 every permission is now in the requested state
|
|
28
|
+
# exit 1 usage error, or a name this platform does not have
|
|
29
|
+
# exit 2 a physical device, where this cannot be done at all
|
|
30
|
+
# exit 3 driver unreachable — run doctor.sh
|
|
31
|
+
# exit 4 the change was applied but did not take
|
|
32
|
+
|
|
33
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
34
|
+
|
|
35
|
+
[ $# -ge 1 ] || die "missing permission (usage: permission.sh <name>=<allow|deny> [...])" 1
|
|
36
|
+
case "$1" in
|
|
37
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
40
|
+
load_config
|
|
41
|
+
BUNDLE="$(cfg '.app.bundle_id' '')"
|
|
42
|
+
[ -n "$BUNDLE" ] || die "app.bundle_id is not set in $DT_CONFIG — permissions are granted to a bundle" 2
|
|
43
|
+
|
|
44
|
+
[ "$(ios_kind)" = simulator ] \
|
|
45
|
+
|| die "a physical iPhone has no host-side permission API — drive the dialog instead: tap: { label: <allow button>, optional: true }" 2
|
|
46
|
+
|
|
47
|
+
UDID="$(device_udid)"
|
|
48
|
+
|
|
49
|
+
# The neutral names, mapped onto Apple's service list. Names Android has and
|
|
50
|
+
# this does not are absent on purpose: refusing is the point, since a permission
|
|
51
|
+
# step that silently did nothing is worse than one that failed.
|
|
52
|
+
ios_service() {
|
|
53
|
+
case "$1" in
|
|
54
|
+
microphone) printf 'microphone' ;;
|
|
55
|
+
location) printf 'location' ;;
|
|
56
|
+
location-always) printf 'location-always' ;;
|
|
57
|
+
contacts) printf 'contacts' ;;
|
|
58
|
+
photos) printf 'photos' ;;
|
|
59
|
+
photos-add) printf 'photos-add' ;;
|
|
60
|
+
calendar) printf 'calendar' ;;
|
|
61
|
+
reminders) printf 'reminders' ;;
|
|
62
|
+
motion) printf 'motion' ;;
|
|
63
|
+
siri) printf 'siri' ;;
|
|
64
|
+
media) printf 'media-library' ;;
|
|
65
|
+
*) return 1 ;;
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
DONE=""
|
|
70
|
+
for pair in "$@"; do
|
|
71
|
+
case "$pair" in
|
|
72
|
+
*=*) ;;
|
|
73
|
+
*) die "expected <name>=<allow|deny>, got '$pair'" 1 ;;
|
|
74
|
+
esac
|
|
75
|
+
name="${pair%%=*}"
|
|
76
|
+
want="${pair#*=}"
|
|
77
|
+
case "$want" in
|
|
78
|
+
allow|deny) ;;
|
|
79
|
+
*) die "'$name' must be allow or deny, got '$want'" 1 ;;
|
|
80
|
+
esac
|
|
81
|
+
|
|
82
|
+
service="$(ios_service "$name" || true)"
|
|
83
|
+
if [ -z "$service" ]; then
|
|
84
|
+
case "$name" in
|
|
85
|
+
camera) die "iOS has no settable camera permission — Apple's privacy service list omits it. Drive the dialog: tap: { label: <allow button>, optional: true }" 1 ;;
|
|
86
|
+
notifications|storage|phone|sms) die "'$name' is an Android permission with no iOS equivalent — remove it from this test, or guard the step with platform:" 1 ;;
|
|
87
|
+
*) die "no such permission name '$name' — known here: microphone location location-always contacts photos photos-add calendar reminders motion siri media" 1 ;;
|
|
88
|
+
esac
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
action=grant
|
|
92
|
+
[ "$want" = deny ] && action=revoke
|
|
93
|
+
|
|
94
|
+
# simctl says nothing useful on success and prints one line on failure, so its
|
|
95
|
+
# output is what gets reported rather than a generic message.
|
|
96
|
+
out="$(xcrun simctl privacy "$UDID" "$action" "$service" "$BUNDLE" 2>&1 || true)"
|
|
97
|
+
case "$out" in
|
|
98
|
+
"") ;;
|
|
99
|
+
*) die "could not $want '$name' for $BUNDLE — $(first_line "$out")" 4 ;;
|
|
100
|
+
esac
|
|
101
|
+
|
|
102
|
+
DONE="$DONE $name=$want"
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
# Apple exposes no way to read a permission back, so unlike the Android adapter
|
|
106
|
+
# this cannot verify the result — only that the command reported no error. Said
|
|
107
|
+
# here rather than left for someone to assume the two platforms check equally.
|
|
108
|
+
printf 'OK permissions for %s —%s (applied, not read back: iOS exposes no query)\n' "$BUNDLE" "$DONE"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# settings.sh — the device conditions a person would change by hand.
|
|
3
|
+
#
|
|
4
|
+
# settings.sh appearance light|dark
|
|
5
|
+
# settings.sh textsize <named size>
|
|
6
|
+
# settings.sh orientation portrait|landscape
|
|
7
|
+
#
|
|
8
|
+
# ON A SIMULATOR these go through simctl and are immediate.
|
|
9
|
+
#
|
|
10
|
+
# ON A PHYSICAL iPHONE THERE IS NO API FOR ANY OF THIS, AND THIS VERB REFUSES.
|
|
11
|
+
#
|
|
12
|
+
# iOS exposes no way to set appearance, text size or rotation on a real device
|
|
13
|
+
# from outside it. The only implementation available is the one a person would
|
|
14
|
+
# use — open Settings, navigate, tap the switch — and that has never been run
|
|
15
|
+
# here. Shipping an untested walk through another app's interface would be a
|
|
16
|
+
# claim this repository does not make: docs/RELEASE.md lists what has evidence
|
|
17
|
+
# beside it, and this would have none.
|
|
18
|
+
#
|
|
19
|
+
# So on a device it exits 2 and says to change it by hand. That is a worse
|
|
20
|
+
# answer than working, and a much better one than appearing to work.
|
|
21
|
+
#
|
|
22
|
+
# exit 0 set
|
|
23
|
+
# exit 1 usage error
|
|
24
|
+
# exit 2 not supported on this target
|
|
25
|
+
# exit 3 the simulator refused it
|
|
26
|
+
|
|
27
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
28
|
+
|
|
29
|
+
case "${1-}" in
|
|
30
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
31
|
+
esac
|
|
32
|
+
KEY="${1-}"; VAL="${2-}"
|
|
33
|
+
[ -n "$KEY" ] && [ -n "$VAL" ] \
|
|
34
|
+
|| die "usage: settings.sh <appearance|textsize|orientation> <value>" 1
|
|
35
|
+
[ $# -eq 2 ] || die "too many arguments (usage: settings.sh <key> <value>)" 1
|
|
36
|
+
|
|
37
|
+
case "$KEY" in
|
|
38
|
+
appearance|textsize|orientation) ;;
|
|
39
|
+
*) die "unknown setting '$KEY' — appearance, textsize or orientation" 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
load_config
|
|
43
|
+
UDID="$(device_udid)"
|
|
44
|
+
|
|
45
|
+
if [ "$(ios_kind)" != simulator ]; then
|
|
46
|
+
die "settings on a physical iPhone is not implemented — iOS exposes no API for it, and driving the Settings app by hand has never been run here. Change '$KEY' on the device, or point the config at a simulator" 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
need_cmd xcrun
|
|
50
|
+
|
|
51
|
+
case "$KEY" in
|
|
52
|
+
appearance)
|
|
53
|
+
case "$VAL" in light|dark) ;; *) die "appearance is light or dark, got '$VAL'" 1 ;; esac
|
|
54
|
+
xcrun simctl ui "$UDID" appearance "$VAL" >/dev/null 2>&1 \
|
|
55
|
+
|| die "simctl refused it — is the simulator booted?" 3
|
|
56
|
+
;;
|
|
57
|
+
textsize)
|
|
58
|
+
# simctl takes a named content size, not a scale. The names are the ones
|
|
59
|
+
# Settings shows, and passing a number gets a message from simctl that does
|
|
60
|
+
# not say so, which is why the list is here.
|
|
61
|
+
case "$VAL" in
|
|
62
|
+
extra-small|small|medium|large|extra-large|extra-extra-large|extra-extra-extra-large) ;;
|
|
63
|
+
accessibility-medium|accessibility-large|accessibility-extra-large) ;;
|
|
64
|
+
accessibility-extra-extra-large|accessibility-extra-extra-extra-large) ;;
|
|
65
|
+
*) die "textsize takes a named size, not a number: extra-small, small, medium, large, extra-large, extra-extra-large, extra-extra-extra-large, or accessibility-medium through accessibility-extra-extra-extra-large — got '$VAL'" 1 ;;
|
|
66
|
+
esac
|
|
67
|
+
xcrun simctl ui "$UDID" content_size "$VAL" >/dev/null 2>&1 \
|
|
68
|
+
|| die "simctl refused it — is the simulator booted?" 3
|
|
69
|
+
;;
|
|
70
|
+
orientation)
|
|
71
|
+
case "$VAL" in portrait|landscape) ;; *) die "orientation is portrait or landscape, got '$VAL'" 1 ;; esac
|
|
72
|
+
die "simctl cannot rotate a simulator — rotate it from the Simulator's Device menu, or use an Android target" 2
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
printf 'OK settings — %s is now %s\n' "$KEY" "$VAL"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# setup.sh — build and sign WebDriverAgent for the configured target.
|
|
3
|
+
#
|
|
4
|
+
# setup.sh [--force]
|
|
5
|
+
#
|
|
6
|
+
# WHY THIS EXISTS AS A COMMAND
|
|
7
|
+
#
|
|
8
|
+
# The README asked a new user to clone WebDriverAgent, rewrite its bundle
|
|
9
|
+
# identifiers with sed, and get an xcodebuild signing invocation right. Every
|
|
10
|
+
# step of that is knowledge this repository already has — it is what CI does on
|
|
11
|
+
# every macOS run, and what the person who set this machine up did once by hand.
|
|
12
|
+
# A hundred lines of prose is a worse copy of a script.
|
|
13
|
+
#
|
|
14
|
+
# WHAT IT WILL NOT DO
|
|
15
|
+
#
|
|
16
|
+
# Sign in to Xcode, and register a device with Apple. Both are interactive
|
|
17
|
+
# credential operations, and a tool that appeared to do them would be lying
|
|
18
|
+
# about where the failure came from. When either is missing the build fails with
|
|
19
|
+
# a message Apple wrote for a different audience, so the log is read here and
|
|
20
|
+
# the actual cause is named.
|
|
21
|
+
#
|
|
22
|
+
# exit 0 WebDriverAgent is built for this target
|
|
23
|
+
# exit 1 usage error
|
|
24
|
+
# exit 2 configuration, checkout, or build problem
|
|
25
|
+
|
|
26
|
+
source "$(dirname "$0")/lib.sh"
|
|
27
|
+
load_config
|
|
28
|
+
|
|
29
|
+
FORCE=0
|
|
30
|
+
while [ $# -gt 0 ]; do
|
|
31
|
+
case "$1" in
|
|
32
|
+
--force) FORCE=1; shift ;;
|
|
33
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
34
|
+
*) die "unknown argument: $1" 1 ;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
need_cmd git
|
|
39
|
+
need_cmd xcodebuild
|
|
40
|
+
|
|
41
|
+
say() { printf '%s %s — %s\n' "$1" "$2" "$3"; }
|
|
42
|
+
|
|
43
|
+
PROJECT="$(expand_path "$(cfg '.wda.project')")"
|
|
44
|
+
WDA_DIR="$(dirname "$PROJECT")"
|
|
45
|
+
PREFIX="$(cfg '.wda.bundle_id_prefix')"
|
|
46
|
+
TEAM="$(cfg '.wda.team_id' '')"
|
|
47
|
+
SCHEME="$(cfg '.wda.scheme' 'WebDriverAgentRunner')"
|
|
48
|
+
KIND="$(ios_kind)"
|
|
49
|
+
UDID="$(device_udid)"
|
|
50
|
+
DERIVED="$(wda_derived)"
|
|
51
|
+
|
|
52
|
+
# --- 1. the checkout ----------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
if [ -d "$PROJECT" ]; then
|
|
55
|
+
say OK checkout "WebDriverAgent already at $WDA_DIR"
|
|
56
|
+
else
|
|
57
|
+
mkdir -p "$(dirname "$WDA_DIR")" || die "could not create $(dirname "$WDA_DIR")" 2
|
|
58
|
+
# --depth 1: nobody here needs its history, and a shallow clone is a tenth of
|
|
59
|
+
# the download on a connection that may be someone's phone.
|
|
60
|
+
git clone --depth 1 https://github.com/appium/WebDriverAgent.git "$WDA_DIR" >/dev/null 2>&1 \
|
|
61
|
+
|| die "could not clone WebDriverAgent into $WDA_DIR — check the path in wda.project and that the directory is writable" 2
|
|
62
|
+
say OK checkout "cloned into $WDA_DIR"
|
|
63
|
+
[ -d "$PROJECT" ] \
|
|
64
|
+
|| die "cloned, but $PROJECT does not exist — wda.project should point at <checkout>/WebDriverAgent.xcodeproj" 2
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# --- 2. the bundle identifiers ------------------------------------------------
|
|
68
|
+
#
|
|
69
|
+
# Upstream ships com.facebook.* identifiers, which nobody outside Meta can sign.
|
|
70
|
+
# The .orig copy is kept because this edits a file the user owns and will one
|
|
71
|
+
# day want to `git pull` over.
|
|
72
|
+
|
|
73
|
+
PBX="$PROJECT/project.pbxproj"
|
|
74
|
+
[ -f "$PBX" ] || die "no project file at $PBX" 2
|
|
75
|
+
|
|
76
|
+
if grep -q 'com\.facebook\.WebDriverAgentRunner' "$PBX" 2>/dev/null || [ "$FORCE" -eq 1 ]; then
|
|
77
|
+
[ -f "$PBX.orig" ] || cp "$PBX" "$PBX.orig"
|
|
78
|
+
sed -i '' \
|
|
79
|
+
-e "s/com\.facebook\.WebDriverAgentRunner/$PREFIX.WebDriverAgentRunner/g" \
|
|
80
|
+
-e "s/com\.facebook\.WebDriverAgentLib/$PREFIX.WebDriverAgentLib/g" \
|
|
81
|
+
"$PBX" \
|
|
82
|
+
|| die "could not rewrite bundle identifiers in $PBX" 2
|
|
83
|
+
say OK identifiers "rewritten to $PREFIX.* (original kept as project.pbxproj.orig)"
|
|
84
|
+
else
|
|
85
|
+
say OK identifiers "already $PREFIX.*"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# --- 3. the build -------------------------------------------------------------
|
|
89
|
+
#
|
|
90
|
+
# A SIMULATOR BUILD IS NOT SIGNED, AND THAT IS THE WHOLE REASON iOS RUNS IN CI.
|
|
91
|
+
#
|
|
92
|
+
# CODE_SIGNING_ALLOWED=NO makes the simulator build need no account, no team and
|
|
93
|
+
# no device registration — which is what lets a hosted macOS runner do this.
|
|
94
|
+
# A device build needs all three, and the command line is where the team goes:
|
|
95
|
+
# the only DEVELOPMENT_TEAM entries in the project belong to a tvOS target, so
|
|
96
|
+
# editing the project file looks like it worked and changes nothing.
|
|
97
|
+
|
|
98
|
+
LOG="$(state_dir)/wda-build.log"
|
|
99
|
+
if [ "$KIND" = simulator ]; then
|
|
100
|
+
DEST="id=$UDID"
|
|
101
|
+
set -- build-for-testing -project "$PROJECT" -scheme "$SCHEME" \
|
|
102
|
+
-destination "$DEST" -derivedDataPath "$DERIVED" CODE_SIGNING_ALLOWED=NO
|
|
103
|
+
say OK target "simulator $UDID — no signing required"
|
|
104
|
+
else
|
|
105
|
+
[ -n "$TEAM" ] \
|
|
106
|
+
|| die "wda.team_id is empty, and a device build must be signed — README.md explains where to find the real Team ID, which is not the value in parentheses in security find-identity" 2
|
|
107
|
+
DEST="id=$UDID"
|
|
108
|
+
set -- build-for-testing -project "$PROJECT" -scheme "$SCHEME" \
|
|
109
|
+
-destination "$DEST" -derivedDataPath "$DERIVED" \
|
|
110
|
+
-allowProvisioningUpdates DEVELOPMENT_TEAM="$TEAM" CODE_SIGN_STYLE=Automatic
|
|
111
|
+
say OK target "device $UDID — signing with team $TEAM"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
printf 'RUN xcodebuild build-for-testing (this takes a few minutes the first time)\n'
|
|
115
|
+
if xcodebuild "$@" >"$LOG" 2>&1; then
|
|
116
|
+
say OK build "WebDriverAgent built into $DERIVED"
|
|
117
|
+
else
|
|
118
|
+
# THE LOG KNOWS WHY. SAY IT.
|
|
119
|
+
#
|
|
120
|
+
# Every string below is one xcodebuild prints, and each has a different
|
|
121
|
+
# remedy. "See the log" is the answer a tool gives when it has not looked, and
|
|
122
|
+
# the log is four hundred lines of which one matters.
|
|
123
|
+
why="the build failed"
|
|
124
|
+
if grep -q 'Unable to find a destination matching' "$LOG" 2>/dev/null; then
|
|
125
|
+
# Found the first time this was run against a real iPhone: the phone was
|
|
126
|
+
# paired and listed by idevice_id, and Xcode still could not see it. That is
|
|
127
|
+
# what a locked screen, a pulled cable or an untrusted computer looks like
|
|
128
|
+
# from here, and the message Apple prints names none of them.
|
|
129
|
+
if [ "$KIND" = simulator ]; then
|
|
130
|
+
why="Xcode has no simulator with UDID $UDID — check device.udid against xcrun simctl list devices"
|
|
131
|
+
# NOT `xctrace … | grep -q`, AND NOT `printf … | grep -q` EITHER.
|
|
132
|
+
#
|
|
133
|
+
# grep -q exits the moment it matches, so the write end takes SIGPIPE.
|
|
134
|
+
# Under `set -o pipefail` — which lib.sh sets — the first spelling reported
|
|
135
|
+
# 141 even though the string was found, and the branch never fired: the
|
|
136
|
+
# message said "never seen" about a device Xcode was listing. Feeding it
|
|
137
|
+
# from printf instead fixed the exit code and left the shell writing into a
|
|
138
|
+
# closed pipe, which prints `printf: write error: Broken pipe` on stderr for
|
|
139
|
+
# every device that IS found. A case pattern has neither problem.
|
|
140
|
+
elif case "$(xcrun xctrace list devices 2>/dev/null)" in *"$UDID"*) true ;; *) false ;; esac; then
|
|
141
|
+
# Xcode knows the device and still will not build for it. Measured: the
|
|
142
|
+
# phone had been unplugged, and it lingers in the device list because it
|
|
143
|
+
# is remembered, not because it is reachable. Saying "no such device"
|
|
144
|
+
# there sends someone to check the UDID, which is correct.
|
|
145
|
+
why="Xcode knows device $UDID but cannot reach it — it is remembered from a previous session, not connected now: plug it in over USB and unlock the screen"
|
|
146
|
+
else
|
|
147
|
+
why="Xcode has never seen device $UDID — attach it over USB, unlock the screen, and trust this computer when it asks; note that idevice_id -l can list a device Xcode does not have"
|
|
148
|
+
fi
|
|
149
|
+
elif grep -q 'No Account for Team' "$LOG" 2>/dev/null; then
|
|
150
|
+
why="no Apple ID in Xcode is a member of team $TEAM — Xcode → Settings → Accounts → +, then run this again. This is the one step that cannot be scripted"
|
|
151
|
+
elif grep -q "isn't registered in your developer account" "$LOG" 2>/dev/null; then
|
|
152
|
+
why="the device is not registered in team $TEAM — register $UDID at developer.apple.com under Devices, in that team, then run this again. Check the UDID character by character: two iPhones of the same model differ only in the middle"
|
|
153
|
+
elif grep -q 'Provisioning profile .* doesn' "$LOG" 2>/dev/null; then
|
|
154
|
+
why="the provisioning profile does not include $UDID — register the device in team $TEAM at developer.apple.com, then run this again"
|
|
155
|
+
elif grep -q 'requires a development team' "$LOG" 2>/dev/null; then
|
|
156
|
+
why="the build wants a development team and wda.team_id is '$TEAM'"
|
|
157
|
+
elif grep -q 'xcode-select' "$LOG" 2>/dev/null; then
|
|
158
|
+
why="the command line tools are selected instead of Xcode — sudo xcode-select --switch /Applications/Xcode.app"
|
|
159
|
+
fi
|
|
160
|
+
die "$why — full log at $LOG" 2
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# --- 4. what came out ---------------------------------------------------------
|
|
164
|
+
#
|
|
165
|
+
# An .xctestrun is what doctor actually runs, so its absence is the failure
|
|
166
|
+
# worth catching here rather than three commands later.
|
|
167
|
+
XCTESTRUN="$(ls "$DERIVED"/Build/Products/*.xctestrun 2>/dev/null | head -1 || true)"
|
|
168
|
+
[ -n "$XCTESTRUN" ] \
|
|
169
|
+
|| die "the build reported success but no .xctestrun is in $DERIVED/Build/Products — full log at $LOG" 2
|
|
170
|
+
say OK product "$(basename "$XCTESTRUN")"
|
|
171
|
+
|
|
172
|
+
if [ "$KIND" = device ]; then
|
|
173
|
+
printf 'NOTE the first launch on a device needs the certificate trusted by hand: Settings → General → VPN & Device Management\n'
|
|
174
|
+
fi
|
|
175
|
+
printf 'NEXT %s\n' "$(as_cmd doctor --recover)"
|