Skip to content

fix: SHA-256 version checks for iOS/Android drivers and app binary cache#3140

Open
qwertey6 wants to merge 3 commits intomobile-dev-inc:mainfrom
ReverentPeer:pr/3-version-safety
Open

fix: SHA-256 version checks for iOS/Android drivers and app binary cache#3140
qwertey6 wants to merge 3 commits intomobile-dev-inc:mainfrom
ReverentPeer:pr/3-version-safety

Conversation

@qwertey6
Copy link
Copy Markdown

@qwertey6 qwertey6 commented Apr 4, 2026

Proposed changes

Makes reinstallDriver=false safe for production. PR #3139 skips driver reinstallation for ~4x faster startup, but without version checks, upgrading Maestro could leave stale drivers on devices — causing silent failures or incompatible API responses. This PR adds SHA-256 hash validation to both platforms.

Impact

  • Prevents stale drivers after Maestro upgrade: automatically detects and replaces outdated drivers
  • Prevents stale app cache after app update: clearAppState no longer silently downgrades apps on cloned simulators
  • Fixes clearState command doesn't reliably clear UserDefaults on iOS #1601clearState reliability on iOS (the cloned simulator binary race)
  • Zero performance cost on the happy path: hash comparison is <1ms, only triggers reinstall on actual mismatch

What could go wrong without this PR

If a user upgrades Maestro and runs maestro test:

  • iOS: isChannelAlive() finds the old XCTest runner still responding → skips reinstall → old runner has incompatible API → mysterious failures
  • Android: isPackageInstalled() finds old APKs → skips install → old gRPC server has different protocol → mysterious failures
  • clearAppState: App binary cached from v1, user installs v2, clearAppState reinstalls from cache → app silently reverted to v1

Changes

iOS XCTest runner version check:

  • XCTestInstaller interface gains isVersionMatch(): Boolean (default true for backward compat)
  • LocalXCTestInstaller computes SHA-256 of build products, writes .running-hash marker on startup, compares on reuse
  • XCTestDriverClient.restartXCTestRunner() checks isChannelAlive() && isVersionMatch() — mismatch triggers fresh start

Android driver version check:

  • AndroidDriver.computeDriverHash() hashes bundled maestro-app.apk + maestro-server.apk
  • Hash stored in ~/.maestro/android-driver-hash after install
  • installMaestroDriverApp() / installMaestroServerApp() check hash before skipping — mismatch forces reinstall

App binary cache staleness:

  • getCachedAppBinary() compares Info.plist of cached vs installed app
  • Stale cache detected → refreshed before reinstall
  • Per-device cache dirs (~/.maestro/app-cache/<deviceId>/) prevent parallel races on cloned simulators

Verification

Scenario Result
iOS: upgrade Maestro, run without --reinstall-driver Driver version mismatch detected, reinstalled automatically
iOS: same version, driver still running Version matches, reused (no reinstall)
iOS: explicit --reinstall-driver Always reinstalls regardless of version
Android: same version, APKs installed Hash matches, skipped (fast)
App updated, clearAppState called Stale cache detected, re-cached from device
Cloned sims parallel clearAppState Per-device cache dirs, no race

Testing

  • 4 new tests in XCTestDriverClientTest using FakeXCTestInstaller (fakes, not mocks — per CONTRIBUTING.md)
  • 3 new tests in LocalSimulatorUtilsTest covering cache staleness detection
  • All existing tests pass

Depends on: #3139 (performance) which depends on #3138 (parallel execution)
Next: PR 4 fixes waitToSettleTimeoutMs on iOS

Issues fixed

Closes #1601

qwertey6 and others added 3 commits April 4, 2026 15:37
iOS simulators share the host's localhost, causing port collisions when
multiple Maestro processes target different sims simultaneously. Session
tracking was per-platform, so two processes on different devices would
interfere with each other's sessions.

Changes:
- Per-device session tracking: SessionStore keys are now
  "{platform}_{deviceId}_{sessionId}" instead of "{platform}_{sessionId}"
- Add --driver-host-port CLI flag for explicit XCTest server port
- Auto-select available ports with isPortAvailable() check
- Refactor SessionStore from singleton to injectable class (DI)
- Add shouldCloseSession(platform, deviceId) for per-device shutdown
  instead of global activeSessions().isEmpty()
- Add cross-process file locking to KeyValueStore (~/.maestro/sessions)
- Append PID to debug log directory to prevent parallel race
- Enable useJUnitPlatform() in maestro-cli (was missing)
- Add SessionStoreTest with 8 tests covering isolation and lifecycle

Verified: 3 iOS simulators + Android emulator running simultaneously,
all passing. Both --driver-host-port (explicit) and auto-port-selection
work correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Default --reinstall-driver to false: reuse a healthy running driver
  instead of killing and reinstalling on every run (~40s saved on iOS)
- XCTestDriverClient checks isChannelAlive() before reinstalling —
  if the user explicitly passes --reinstall-driver, honor it
- Cache extracted iOS build products per-device in
  ~/.maestro/build-products/<deviceId>/ with SHA-256 hash validation:
  skips extraction when source matches cache, re-extracts on upgrade
- Reduce XCTest status check HTTP read timeout from 100s to 3s
- Remove Thread.sleep(1000) heartbeat delay hack (no longer needed
  with per-device session tracking)

Single device: ~52s → ~10-12s. Three devices parallel: ~54s → ~18s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- iOS XCTest runner: add isVersionMatch() to XCTestInstaller interface.
  LocalXCTestInstaller compares SHA-256 hash of build products against
  a .running-hash marker written at startup. restartXCTestRunner now
  checks both isChannelAlive() AND isVersionMatch() — stale runners
  from a previous Maestro version are replaced automatically.

- Android driver: add isDriverVersionCurrent() that hashes the bundled
  maestro-app.apk and maestro-server.apk, compares against stored hash
  in ~/.maestro/android-driver-hash. On mismatch, APKs are reinstalled
  even when reinstallDriver=false.

- App binary cache (clearAppState): getCachedAppBinary now compares
  Info.plist of cached vs installed app. Stale cache from app updates
  is detected and refreshed before reinstall. Per-device cache dirs
  (~/.maestro/app-cache/<deviceId>/) prevent parallel races.

- Add XCTestDriverClientTest (4 tests) and LocalSimulatorUtilsTest
  (3 tests) covering version mismatch, reuse, and cache behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

clearState command doesn't reliably clear UserDefaults on iOS

1 participant