feat(install): alternative installproxy install method via AFC staging - #810
Open
danielpaulus wants to merge 2 commits into
Open
feat(install): alternative installproxy install method via AFC staging#810danielpaulus wants to merge 2 commits into
danielpaulus wants to merge 2 commits into
Conversation
Introduce an alternative app installation path modeled after pymobiledevice3: upload the .ipa as-is via AFC to PublicStaging/<name>, then send an installation_proxy Install command with PackagePath and ClientOptions and stream PercentComplete progress until the install completes or fails. The compressed .ipa is transferred without unpacking it on the host, which is faster for large bundles over USB2. - installationproxy: new Connection.Install (Install command + progress streaming) and InstallIpa (AFC staging + install orchestration) - afc: File now implements io.ReaderFrom, chunking uploads into fileWrite packets of at most 64KiB; WriteToFile uses it - cli: ios install gains --method=<method> with values 'zipconduit' (default, unchanged) and 'installproxy' (.ipa only) - unit tests for the Install request plist, progress/completion/error parsing against canned plists, and AFC upload chunking against in-memory connection doubles Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Owner
Author
|
/test-devices |
Contributor
Contributor
|
❌ Real-device tests failed — see run. |
The installproxy method uploaded the .ipa to PublicStaging via AFC but never deleted it, leaking device storage on every install (an .ipa can be hundreds of MB). Delete the staged package after installation via a deferred AFC Remove, whether the install succeeds or fails, matching pymobiledevice3's finally-block cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Owner
Author
Adversarial review of PR #810 (installproxy install via AFC staging)Reviewed the installation_proxy protocol against pymobiledevice3 and libimobiledevice, the AFC chunked-upload change, and the response loop. Pushed one fix; the rest of the flagged items are false positives. Fix applied (1163639)
Findings verified as correct / dismissed
Verification
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
App installs currently go exclusively through zipconduit, which requires unpacking the bundle on the host and streaming it uncompressed to the device. USB transfers are capped at USB2 speeds (~30-35 MB/s), so sending the uncompressed payload is the bottleneck for large apps. Issue #400 asks for an alternative install path based on AFC + installation_proxy, like pymobiledevice3, which transfers the compressed .ipa as-is and lets the device do the unpacking.
Design
The new path mirrors pymobiledevice3's
install_from_local:PublicStagingexists (AFC root is/var/mobile/Media), and upload the .ipa unchanged toPublicStaging/<basename>.com.apple.mobile.installation_proxyand send{"Command": "Install", "PackagePath": "PublicStaging/<basename>", "ClientOptions": {}}.Status/PercentComplete, untilStatus == "Complete"or anError/ErrorDescriptionresponse, which is surfaced as an error.zipconduit remains the default; the new method is opt-in via
ios install --path=<ipa> --method=installproxy. The installproxy method supports .ipa files only — for.appfolders it returns a clear error pointing at the default method.Implementation
ios/installationproxy/install.go:Connection.Install(packagePath, options)— sends the Install command and blocks streaming progress until complete or error, following the existingUninstallpattern (PlistCodec +ios.ParsePlist).InstallIpa(device, ipaPath)— orchestrates AFC staging (Stat/MkDirPublicStaging, upload) and the Install command.ClientOptionsis always sent (empty dict when no options).installCommand/evaluateInstallProgressare separate small funcs so the request construction and response parsing are unit-testable.ios/afc/client.go:Filenow implementsio.ReaderFrom, splitting uploads intofileWritepackets of at most 64KiB payload each (matching common AFC implementations), so staging a large .ipa never produces oversized AFC packets.Client.WriteToFileuses it.--method=<method>onios install(cmd_device_apps.go, docopt usage + help text inmain.go,internal/clihelp/help.yaml, regeneratedtestdata/help/global.golden). Unknown values fail fast with the supported values listed.Options considered
--method=installproxy(chosen): a single flag with named values keepsios installas one command, defaults to the existing behavior, and leaves room for future methods without adding a flag per transport.--installproxyboolean flag: simplest, but a second method later would mean mutually-exclusive booleans and docopt disambiguation pain.ios install-ipa/ios installproxy install): splits one user intent ("install this app") across commands and doubles the docs/registry surface.Test plan
go build ./...,go test ./...(all green),gofmt -lclean on changed files.ios/installationproxy/install_test.go: Install request plist construction (Command/PackagePath/ClientOptions, including the always-present empty dict) asserted by decoding the bytes written to an in-memory connection; progress → completion streaming; error responses (Error+ErrorDescription) surfaced as errors; progress/complete/error parsing against canned XML plists; unknown updates rejected.ios/afc/upload_unit_test.go:File.ReadFromchunking against an in-memory acking connection double — 2×64KiB+1234 bytes yields exactly threefileWritepackets with correct handle, sizes, and byte-identical reassembled payload; empty upload sends nothing.--method=installproxyparsing, unknown method rejection.--method=installproxyon the device runners) is the next step; this PR intentionally ships the device-free implementation and unit coverage first.Fixes #400
🤖 Generated with Claude Code
https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk