From abaf8ce171883268127966004849e24661c45c9d Mon Sep 17 00:00:00 2001 From: MxScripter <14134362+MxScripter@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:06:15 +0200 Subject: [PATCH] fix(devicelab-ios): keep permission grants when launchApp uses the default all:allow launchApp reset every permission on the simulator and then granted `all` with simctl. simctl's contacts grant does not take (iOS 18.6 simulator) and it has no notifications service, so a grant made any other way (the user's tap on the prompt, applesimutils, an earlier run) was lost on every launch, and an app that shows a permission onboarding while a permission is undecided opened on it after every launchApp. With all:allow nothing is meant to stay undecided, so skip the reset. An explicit permissions: map still resets first. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + pkg/driver/devicelab_ios/lifecycle.go | 19 +++++++++++++++++-- pkg/driver/devicelab_ios/lifecycle_test.go | 4 ++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52c2549..10da6e5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed +- **DeviceLab iOS: `launchApp` with the default `all: allow` no longer resets the app's permissions on a simulator.** It reset every permission and then granted `all` with `simctl`, but simctl's contacts grant does not take (iOS 18.6 simulator) and it has no notifications service, so a grant made another way (the user's tap on the prompt, applesimutils, an earlier run) was lost on every launch and the app asked again: an app that shows a permission onboarding when a permission is undecided opened on it after every `launchApp`. An explicit `permissions:` map still resets first, so permissions it doesn't name stay undecided. - **UIAutomator2 `hideKeyboard` no longer trusts a single "keyboard hidden" read.** While the keyboard is still coming up, `dumpsys window InputMethod` can report it not shown and Appium's hide_keyboard can answer 404, so the step returned without pressing BACK and the keyboard stayed over the next target. The driver now treats the keyboard as hidden only when two reads 300 ms apart agree, both before and after Appium's call, and still presses BACK only while the keyboard is shown. - **WDA keeps enough idle connections for its own parallel reads.** The driver reads an element's name, rect, text and displayed at once, and a tap looks an element up four ways at once, but Go's default transport keeps two idle connections per host, so every burst closed two connections and opened two new ones. Through a forwarded port to a physical iPhone, new connections opened together fail with EOF and are sent again, which costs time on every step. The WDA client now keeps up to eight. - **`retry` counts retries, not attempts, as Maestro does.** `maxRetries: 1` now runs the commands twice (once, then one retry), an unset `maxRetries` means one retry, and the value is capped at 3. The runner ran exactly `maxRetries` attempts, three when unset and with no cap, so `maxRetries: 1` never retried. A value that is not an integer is logged and read as 1 instead of failing the step. diff --git a/pkg/driver/devicelab_ios/lifecycle.go b/pkg/driver/devicelab_ios/lifecycle.go index 471dbb26..82e96619 100644 --- a/pkg/driver/devicelab_ios/lifecycle.go +++ b/pkg/driver/devicelab_ios/lifecycle.go @@ -335,8 +335,14 @@ func (d *Driver) applyLaunchPermissions(bid string, perms map[string]string) { if len(perms) == 0 { perms = map[string]string{"all": "allow"} } - if _, err := d.runSimctl("privacy", d.udid, "reset", "all", bid); err != nil { - logger.Warn("launchApp: permission reset failed: %v", err) + // With all:allow nothing is meant to stay "not determined", so the reset can only lose grants, and + // simctl cannot put all of them back: its contacts grant does not take (iOS 18.6 simulator) and it has + // no notifications service. Grants made another way (the user's tap on the prompt, applesimutils, an + // earlier run) are kept instead of being asked for again on every launch. + if !hasOnlyAllAllow(perms) { + if _, err := d.runSimctl("privacy", d.udid, "reset", "all", bid); err != nil { + logger.Warn("launchApp: permission reset failed: %v", err) + } } set := map[string]string{} for name, value := range perms { @@ -349,6 +355,15 @@ func (d *Driver) applyLaunchPermissions(bid string, perms map[string]string) { } } +// hasOnlyAllAllow reports whether perms is just all:allow, launchApp's default. +func hasOnlyAllAllow(perms map[string]string) bool { + if len(perms) != 1 { + return false + } + value, ok := perms["all"] + return ok && strings.EqualFold(strings.TrimSpace(value), "allow") +} + // applyPermissions sets each permission with `simctl privacy`, one service // per call, in a stable order. Names iOS gives the host no control over // (notifications, faceid) are logged and skipped. diff --git a/pkg/driver/devicelab_ios/lifecycle_test.go b/pkg/driver/devicelab_ios/lifecycle_test.go index cd99c594..e4bc6a14 100644 --- a/pkg/driver/devicelab_ios/lifecycle_test.go +++ b/pkg/driver/devicelab_ios/lifecycle_test.go @@ -71,6 +71,10 @@ func TestLaunchAppDefaultsAndErrors(t *testing.T) { if !sl.has("privacy SIM-1 grant all com.x") { t.Fatalf("default all:allow missing: %v", sl.calls) } + // A reset would lose grants that simctl cannot give back (contacts, notifications). + if sl.has("privacy SIM-1 reset") { + t.Fatalf("default all:allow must not reset permissions: %v", sl.calls) + } if sl.has("terminate") { t.Fatal("stopApp: false must not terminate") }