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
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cold-start stress on iOS Simulator.
|
||||
#
|
||||
# Usage:
|
||||
# ./Scripts/cold-start-stress.sh [COUNT=100] [DEVICE_NAME=iPhone 17]
|
||||
#
|
||||
# Method: terminate → simctl launch (host cold start that auto-arms PiP).
|
||||
# Keyboard URL wake (osgkeyboard://startflow) is optional once scheme is approved.
|
||||
#
|
||||
# Limitations (documented in summary):
|
||||
# - Simulator VideoCall PiP is typically `unsupported` — not a device signal.
|
||||
# - Keyboard extension process (KVC.init) is not spawned here.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
COUNT="${1:-100}"
|
||||
DEVICE_NAME="${2:-iPhone 17}"
|
||||
BUNDLE="com.osgkeyboard.ios"
|
||||
OUT_DIR="${ROOT}/.tmp/cold-start-stress-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$OUT_DIR"
|
||||
REPORT="$OUT_DIR/report.jsonl"
|
||||
SUMMARY="$OUT_DIR/summary.txt"
|
||||
LOG_FILE="$OUT_DIR/unified.log"
|
||||
|
||||
echo "==> Out: $OUT_DIR"
|
||||
echo "==> Count: $COUNT Device: '$DEVICE_NAME'"
|
||||
|
||||
# Exact device name match (avoid "iPhone 17" → "iPhone 17 Pro")
|
||||
UDID="$(xcrun simctl list devices available | awk -F '[()]' -v n="$DEVICE_NAME" '
|
||||
{
|
||||
line=$0
|
||||
# strip leading spaces
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
name=line
|
||||
sub(/ \(.*/, "", name)
|
||||
if (name == n && line ~ /Booted/) { print $2; exit }
|
||||
}
|
||||
')"
|
||||
if [[ -z "$UDID" ]]; then
|
||||
UDID="$(xcrun simctl list devices available | awk -F '[()]' -v n="$DEVICE_NAME" '
|
||||
{
|
||||
line=$0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
name=line
|
||||
sub(/ \(.*/, "", name)
|
||||
if (name == n && line !~ /unavailable/) { print $2; exit }
|
||||
}
|
||||
')"
|
||||
fi
|
||||
if [[ -z "$UDID" ]]; then
|
||||
echo "error: exact device '$DEVICE_NAME' not found" >&2
|
||||
xcrun simctl list devices available | grep -i iphone | head -40 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "==> UDID: $UDID"
|
||||
|
||||
echo "==> Booting simulator"
|
||||
xcrun simctl boot "$UDID" 2>/dev/null || true
|
||||
xcrun simctl bootstatus "$UDID" -b
|
||||
|
||||
# Prefer signed cold-start DerivedData build, else any existing Debug-iphonesimulator app
|
||||
APP=""
|
||||
if [[ -d "$ROOT/.derivedData-cold-start/Build/Products/Debug-iphonesimulator/OSGKeyboard.app" ]]; then
|
||||
APP="$ROOT/.derivedData-cold-start/Build/Products/Debug-iphonesimulator/OSGKeyboard.app"
|
||||
else
|
||||
APP="$(ls -d "$HOME"/Library/Developer/Xcode/DerivedData/OSGKeyboard-*/Build/Products/Debug-iphonesimulator/OSGKeyboard.app 2>/dev/null | head -1 || true)"
|
||||
fi
|
||||
if [[ -z "$APP" || ! -d "$APP" ]]; then
|
||||
echo "error: no built OSGKeyboard.app — build for simulator first" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> Installing $APP"
|
||||
xcrun simctl install "$UDID" "$APP" >/dev/null
|
||||
xcrun simctl privacy "$UDID" grant microphone "$BUNDLE" 2>/dev/null || true
|
||||
|
||||
seed_onboarding() {
|
||||
echo "==> Seeding onboarding (App Group + Keychain path)"
|
||||
xcrun simctl launch "$UDID" "$BUNDLE" >/dev/null || true
|
||||
sleep 2
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
|
||||
local group_container prefs
|
||||
group_container="$(xcrun simctl get_app_container "$UDID" "$BUNDLE" group.com.osgkeyboard.shared 2>/dev/null || true)"
|
||||
if [[ -z "$group_container" || ! -d "$group_container" ]]; then
|
||||
echo "warning: App Group container missing" >&2
|
||||
return 0
|
||||
fi
|
||||
prefs="$group_container/Library/Preferences/group.com.osgkeyboard.shared.plist"
|
||||
mkdir -p "$(dirname "$prefs")"
|
||||
if [[ -f "$prefs" ]]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :config.hasCompletedOnboarding true" "$prefs" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Add :config.hasCompletedOnboarding bool true" "$prefs" 2>/dev/null || true
|
||||
else
|
||||
/usr/libexec/PlistBuddy -c "Add :config.hasCompletedOnboarding bool true" "$prefs" 2>/dev/null || true
|
||||
fi
|
||||
echo "==> Seeded hasCompletedOnboarding=$(/usr/libexec/PlistBuddy -c 'Print :config.hasCompletedOnboarding' "$prefs" 2>/dev/null || echo missing)"
|
||||
|
||||
# One launch so ProviderConfig mirrors App Group → Keychain
|
||||
xcrun simctl launch "$UDID" "$BUNDLE" >/dev/null || true
|
||||
sleep 2
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE" 2>/dev/null || true
|
||||
sleep 0.4
|
||||
}
|
||||
seed_onboarding
|
||||
|
||||
: >"$LOG_FILE"
|
||||
xcrun simctl spawn "$UDID" log stream \
|
||||
--style compact \
|
||||
--level debug \
|
||||
--predicate 'process == "OSGKeyboard"' \
|
||||
>"$LOG_FILE" 2>&1 &
|
||||
LOG_PID=$!
|
||||
trap 'kill $LOG_PID 2>/dev/null || true' EXIT
|
||||
sleep 1
|
||||
|
||||
logfile_window() {
|
||||
local offset="$1"
|
||||
local start=$((offset + 1))
|
||||
tail -c +"$start" "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Returns: host|pip
|
||||
# host: success|fail|unknown
|
||||
# pip: success|unsupported|fail|permissions|onboarding|unknown
|
||||
logfile_classify() {
|
||||
local chunk="$1"
|
||||
local host="unknown" pip="unknown"
|
||||
|
||||
# Use here-string (not pipe) to avoid SIGPIPE under `set -o pipefail`.
|
||||
if grep -Eq "OSGKeyboardApp\.init" <<<"$chunk"; then
|
||||
host="success"
|
||||
fi
|
||||
if grep -Eq "MainAppRoot\.onAppear skip Flow.*onboarding incomplete" <<<"$chunk"; then
|
||||
pip="onboarding"
|
||||
echo "$host|$pip"
|
||||
return
|
||||
fi
|
||||
if grep -Eq "activateOnForeground aborted reason=permissions|startSessionAsync\.blocked.*permissions" <<<"$chunk"; then
|
||||
pip="permissions"
|
||||
echo "$host|$pip"
|
||||
return
|
||||
fi
|
||||
if grep -Eq "Flow session started \(PiP keep-alive\)|low-profile PiP active|startSessionAsync\.ready" <<<"$chunk"; then
|
||||
pip="success"
|
||||
elif grep -Eq "failure=unsupported|PiP keep-alive failed to start: unsupported" <<<"$chunk"; then
|
||||
pip="unsupported"
|
||||
elif grep -Eq "PiP keep-alive failed to start|PiP startAndWait failed|startSessionAsync\.failed.*pipUnavailable" <<<"$chunk"; then
|
||||
pip="fail"
|
||||
elif grep -Eq "activateOnForeground|startSession\.request.*autoPiP|startSessionAsync\.begin" <<<"$chunk"; then
|
||||
pip="seen_no_result"
|
||||
fi
|
||||
|
||||
echo "$host|$pip"
|
||||
}
|
||||
|
||||
HOST_OK=0
|
||||
HOST_FAIL=0
|
||||
PIP_SUCCESS=0
|
||||
PIP_UNSUPPORTED=0
|
||||
PIP_FAIL=0
|
||||
PIP_PERMISSIONS=0
|
||||
PIP_ONBOARDING=0
|
||||
PIP_OTHER=0
|
||||
|
||||
echo "==> Running $COUNT cold starts (terminate → launch)"
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
xcrun simctl terminate "$UDID" "$BUNDLE" 2>/dev/null || true
|
||||
sleep 0.35
|
||||
|
||||
OFFSET=$(wc -c <"$LOG_FILE" | tr -d ' ')
|
||||
START_TS=$(date +%s)
|
||||
|
||||
if ! xcrun simctl launch "$UDID" "$BUNDLE" >/dev/null 2>"$OUT_DIR/launch-$i.err"; then
|
||||
printf '{"i":%d,"host":"fail","pip":"unknown","elapsed_s":0,"note":"launch_failed"}\n' "$i" >>"$REPORT"
|
||||
printf "[%3d/%d] host=fail pip=unknown\n" "$i" "$COUNT"
|
||||
HOST_FAIL=$((HOST_FAIL + 1))
|
||||
PIP_OTHER=$((PIP_OTHER + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
RESULT="unknown|unknown"
|
||||
for _ in $(seq 1 50); do # ~15s
|
||||
sleep 0.3
|
||||
CHUNK="$(logfile_window "$OFFSET")"
|
||||
RESULT="$(logfile_classify "$CHUNK")"
|
||||
PIP="${RESULT##*|}"
|
||||
if [[ "$PIP" == "success" || "$PIP" == "unsupported" || "$PIP" == "fail" || "$PIP" == "permissions" || "$PIP" == "onboarding" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
ELAPSED=$(( $(date +%s) - START_TS ))
|
||||
HOST="${RESULT%%|*}"
|
||||
PIP="${RESULT##*|}"
|
||||
|
||||
case "$HOST" in
|
||||
success) HOST_OK=$((HOST_OK + 1)) ;;
|
||||
*) HOST_FAIL=$((HOST_FAIL + 1)) ;;
|
||||
esac
|
||||
case "$PIP" in
|
||||
success) PIP_SUCCESS=$((PIP_SUCCESS + 1)) ;;
|
||||
unsupported) PIP_UNSUPPORTED=$((PIP_UNSUPPORTED + 1)) ;;
|
||||
fail) PIP_FAIL=$((PIP_FAIL + 1)) ;;
|
||||
permissions) PIP_PERMISSIONS=$((PIP_PERMISSIONS + 1)) ;;
|
||||
onboarding) PIP_ONBOARDING=$((PIP_ONBOARDING + 1)) ;;
|
||||
*) PIP_OTHER=$((PIP_OTHER + 1)) ;;
|
||||
esac
|
||||
|
||||
printf '{"i":%d,"host":"%s","pip":"%s","elapsed_s":%d}\n' "$i" "$HOST" "$PIP" "$ELAPSED" >>"$REPORT"
|
||||
printf "[%3d/%d] host=%-8s pip=%-12s %2ds\n" "$i" "$COUNT" "$HOST" "$PIP" "$ELAPSED"
|
||||
done
|
||||
|
||||
{
|
||||
echo "Cold-start stress summary"
|
||||
echo "device=$DEVICE_NAME udid=$UDID count=$COUNT"
|
||||
echo "method=terminate + simctl launch (host cold start / autoPiP path)"
|
||||
echo
|
||||
echo "Host cold start (OSGKeyboardApp.init observed):"
|
||||
echo " success=$HOST_OK fail=$HOST_FAIL"
|
||||
echo " fail_rate=$(python3 -c "print(f'{$HOST_FAIL/$COUNT*100:.1f}%')")"
|
||||
echo
|
||||
echo "PiP keep-alive outcome:"
|
||||
echo " success=$PIP_SUCCESS"
|
||||
echo " unsupported=$PIP_UNSUPPORTED (expected on Simulator)"
|
||||
echo " fail_other=$PIP_FAIL"
|
||||
echo " permissions=$PIP_PERMISSIONS"
|
||||
echo " onboarding=$PIP_ONBOARDING"
|
||||
echo " unknown/other=$PIP_OTHER"
|
||||
echo " non-unsupported fail_rate=$(python3 -c "print(f'{($PIP_FAIL+$PIP_PERMISSIONS+$PIP_ONBOARDING+$PIP_OTHER)/$COUNT*100:.1f}%')")"
|
||||
echo
|
||||
echo "Artifacts: $OUT_DIR"
|
||||
echo "Caveats:"
|
||||
echo " - Simulator PiP is typically 'unsupported'; device PiP is the release gate."
|
||||
echo " - Keyboard extension KVC.init / openHostApp is not covered (needs physical keyboard enablement)."
|
||||
echo " - Host wake path covered = cold launch of the process the keyboard would open."
|
||||
} | tee "$SUMMARY"
|
||||
|
||||
kill "$LOG_PID" 2>/dev/null || true
|
||||
trap - EXIT
|
||||
echo "==> Done"
|
||||
Reference in New Issue
Block a user