feat(keyboard): harden clipboard command and add What's New sheet
Stabilize clipboard long-press prepare/resume across paste alerts and cold start, add an in-app release-notes sheet with remote bilingual HTML, localize typing input settings, and bump build to 55.
This commit is contained in:
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
# Physical-device PiP / host-wake stress using `devicectl --console` logs.
|
||||
#
|
||||
# Usage:
|
||||
# ./Scripts/device-pip-stress.sh [UDID] [COUNT=50]
|
||||
#
|
||||
# Suites:
|
||||
# cold — terminate-existing launch (force-quit / cold start)
|
||||
# bgfg — open Safari (background) then relaunch OSG (foreground restore)
|
||||
# hold — start PiP, wait 20s in session, relaunch to verify still recoverable
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
UDID="${1:-00008130-001C249C0E52001C}"
|
||||
COUNT="${2:-50}"
|
||||
BUNDLE="com.osgkeyboard.ios"
|
||||
SAFARI="com.apple.mobilesafari"
|
||||
OUT_DIR="${ROOT}/.tmp/device-pip-stress-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$OUT_DIR"
|
||||
REPORT="$OUT_DIR/report.jsonl"
|
||||
SUMMARY="$OUT_DIR/summary.txt"
|
||||
APP="$ROOT/.derivedData-device-stress/Build/Products/Debug-iphoneos/OSGKeyboard.app"
|
||||
|
||||
echo "==> Out: $OUT_DIR"
|
||||
echo "==> Device: $UDID count/suite: $COUNT"
|
||||
|
||||
if [[ ! -d "$APP" ]]; then
|
||||
echo "error: missing $APP — build for device first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Ensuring install"
|
||||
xcrun devicectl device install app --device "$UDID" "$APP" --timeout 180 >/dev/null
|
||||
|
||||
# Capture one console launch (timeout seconds). Prints raw console to stdout file.
|
||||
console_launch() {
|
||||
local outfile="$1"
|
||||
local secs="${2:-14}"
|
||||
local extra_flags="${3:-}" # e.g. --terminate-existing
|
||||
# shellcheck disable=SC2086
|
||||
timeout "$secs" xcrun devicectl device process launch \
|
||||
--device "$UDID" \
|
||||
--console \
|
||||
$extra_flags \
|
||||
"$BUNDLE" >"$outfile" 2>&1 || true
|
||||
}
|
||||
|
||||
classify_file() {
|
||||
local f="$1"
|
||||
local host="unknown" pip="unknown" mic="unknown"
|
||||
|
||||
if grep -Eq "OSGKeyboardApp\.init|MainAppRoot\.onAppear|activateOnForeground" "$f"; then
|
||||
host="success"
|
||||
elif grep -Eq "Launched application with com\.osgkeyboard\.ios" "$f"; then
|
||||
host="launch_only"
|
||||
fi
|
||||
|
||||
if grep -Eq "onboarding incomplete" "$f"; then
|
||||
pip="onboarding"
|
||||
elif grep -Eq "aborted reason=permissions|blocked.*permissions" "$f"; then
|
||||
pip="permissions"
|
||||
elif grep -Eq "Flow session started \(PiP keep-alive\)|low-profile PiP active|startSessionAsync\.ready" "$f"; then
|
||||
pip="success"
|
||||
elif grep -Eq "failure=unsupported|failed to start: unsupported" "$f"; then
|
||||
pip="unsupported"
|
||||
elif grep -Eq "PiP keep-alive failed to start|startSessionAsync\.failed.*pipUnavailable|startAndWait failed" "$f"; then
|
||||
pip="fail"
|
||||
elif grep -Eq "PiP start attempt failed" "$f"; then
|
||||
# Retry path — only fail if we never saw success above
|
||||
pip="retry_then_unknown"
|
||||
elif grep -Eq "activateOnForeground|autoPiP|startSessionAsync\.begin" "$f"; then
|
||||
pip="seen_no_result"
|
||||
fi
|
||||
|
||||
# Mic keep-alive contract: idle releases mic after PiP proves
|
||||
if grep -Eq "mic released between utterances|released audio session and frame pump" "$f"; then
|
||||
mic="released_ok"
|
||||
elif grep -Eq "PiP audio session ready" "$f"; then
|
||||
mic="armed"
|
||||
else
|
||||
mic="unknown"
|
||||
fi
|
||||
|
||||
# Promote retry_then_unknown if success markers appeared (grep order already handled)
|
||||
if [[ "$pip" == "retry_then_unknown" ]]; then
|
||||
if grep -Eq "low-profile PiP active|Flow session started \(PiP keep-alive\)" "$f"; then
|
||||
pip="success"
|
||||
else
|
||||
pip="fail"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$host|$pip|$mic"
|
||||
}
|
||||
|
||||
run_suite() {
|
||||
local suite="$1"
|
||||
local i outfile result host pip mic start_ts elapsed flags
|
||||
echo "==> Suite: $suite × $COUNT"
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
outfile="$OUT_DIR/${suite}-$i.console.log"
|
||||
start_ts=$(date +%s)
|
||||
flags=""
|
||||
|
||||
case "$suite" in
|
||||
cold)
|
||||
flags="--terminate-existing"
|
||||
console_launch "$outfile" 14 "$flags"
|
||||
;;
|
||||
bgfg)
|
||||
# Ensure app running with PiP first
|
||||
console_launch "$OUT_DIR/${suite}-$i.prep.log" 12 "--terminate-existing" >/dev/null || true
|
||||
# Background by opening Safari
|
||||
xcrun devicectl device process launch --device "$UDID" "$SAFARI" >/dev/null 2>&1 || true
|
||||
sleep 3
|
||||
# Resume OSG without terminate
|
||||
console_launch "$outfile" 12 ""
|
||||
;;
|
||||
hold)
|
||||
console_launch "$OUT_DIR/${suite}-$i.prep.log" 12 "--terminate-existing" >/dev/null || true
|
||||
# Leave session alive ~20s (PiP should keep host)
|
||||
sleep 20
|
||||
# Background briefly then resume
|
||||
xcrun devicectl device process launch --device "$UDID" "$SAFARI" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
console_launch "$outfile" 12 ""
|
||||
;;
|
||||
esac
|
||||
|
||||
elapsed=$(( $(date +%s) - start_ts ))
|
||||
result="$(classify_file "$outfile")"
|
||||
host="${result%%|*}"
|
||||
rest="${result#*|}"
|
||||
pip="${rest%%|*}"
|
||||
mic="${rest##*|}"
|
||||
|
||||
printf '{"suite":"%s","i":%d,"host":"%s","pip":"%s","mic":"%s","elapsed_s":%d}\n' \
|
||||
"$suite" "$i" "$host" "$pip" "$mic" "$elapsed" >>"$REPORT"
|
||||
printf "[%s %3d/%d] host=%-11s pip=%-12s mic=%-12s %2ds\n" \
|
||||
"$suite" "$i" "$COUNT" "$host" "$pip" "$mic" "$elapsed"
|
||||
done
|
||||
}
|
||||
|
||||
: >"$REPORT"
|
||||
run_suite cold
|
||||
run_suite bgfg
|
||||
run_suite hold
|
||||
|
||||
python3 - "$REPORT" "$SUMMARY" "$UDID" "$COUNT" "$OUT_DIR" <<'PY'
|
||||
import json, collections, sys
|
||||
from pathlib import Path
|
||||
report, summary, udid, count, out = sys.argv[1:6]
|
||||
rows = [json.loads(l) for l in Path(report).read_text().splitlines() if l.strip()]
|
||||
by = collections.defaultdict(list)
|
||||
for r in rows:
|
||||
by[r["suite"]].append(r)
|
||||
|
||||
lines = [
|
||||
"Device PiP / mic keep-alive stress summary",
|
||||
f"device=Rocky 15 PM udid={udid} count_per_suite={count}",
|
||||
f"total_rows={len(rows)}",
|
||||
"",
|
||||
]
|
||||
for suite in ("cold", "bgfg", "hold"):
|
||||
rs = by.get(suite, [])
|
||||
n = max(len(rs), 1)
|
||||
host_ok = sum(1 for r in rs if r["host"] in ("success", "launch_only"))
|
||||
pip_c = collections.Counter(r["pip"] for r in rs)
|
||||
mic_c = collections.Counter(r["mic"] for r in rs)
|
||||
pip_ok = pip_c.get("success", 0)
|
||||
lines += [
|
||||
f"[{suite}]",
|
||||
f" host_ok={host_ok}/{len(rs)} ({host_ok/n*100:.1f}%)",
|
||||
f" pip_success={pip_ok}/{len(rs)} ({pip_ok/n*100:.1f}%) breakdown={dict(pip_c)}",
|
||||
f" mic={dict(mic_c)}",
|
||||
"",
|
||||
]
|
||||
lines += [
|
||||
f"Artifacts: {out}",
|
||||
"Notes:",
|
||||
" - cold uses --terminate-existing (force-quit recovery).",
|
||||
" - bgfg backgrounds via Safari then resumes.",
|
||||
" - hold keeps session ~20s then Safari background + resume.",
|
||||
" - Keyboard-extension mic tap is not automated.",
|
||||
]
|
||||
text = "\n".join(lines) + "\n"
|
||||
Path(summary).write_text(text)
|
||||
print(text)
|
||||
PY
|
||||
|
||||
echo "==> Done"
|
||||
Reference in New Issue
Block a user