Skip to content

Stability fixes for the install/pairing flow - #98

Open
logikodied wants to merge 7 commits into
vyvir:mainfrom
logikodied:fix/install-monitoring-stability
Open

Stability fixes for the install/pairing flow#98
logikodied wants to merge 7 commits into
vyvir:mainfrom
logikodied:fix/install-monitoring-stability

Conversation

@logikodied

@logikodied logikodied commented Jul 14, 2026

Copy link
Copy Markdown

Hey! 👋

Quick heads up before anything else: I'm not a hardcore dev, and I worked through these changes with Claude (Claude Code). I kept running into the flaky pairing/install behavior that others have mentioned too, where sometimes it works and sometimes it just doesn't. Instead of only patching my own machine I figured I'd try to clean it up properly and send it back, in hopes it makes althea more stable for future users. No shame in the AI assist, but I want to be upfront about it. Happy to change anything you don't like.

Each fix is its own commit so they're easy to review or drop individually:

  • The UI froze during installs. The install monitor was a blocking while loop that kept spawning grep subprocesses against the log file. It's now a GLib timeout that reads the log in-process, so the window stays responsive and the 2FA / "are you sure" prompts show up reliably instead of racing the loop.
  • Passwords went through the shell. AltServer was launched with an f-string shell command with the Apple ID and password pasted straight in, so a password with a space or a shell character could break the whole thing. It uses an argv list now, no shell involved. When coreutils' stdbuf is available it line-buffers the output so prompts reach the log faster, and when it's not, the launch still works.
  • Crash when no device was connected. version.parse() blew up trying to parse the fallback string from ideviceinfo. There's now a helper that returns None and shows a "connect and unlock your device" message instead.
  • iOS versions were compared as plain strings, which breaks with double digit versions. The update check in notify() had the same problem and no timeout, fixed both.
  • Path handling cleanup: respects XDG_DATA_HOME, real paths instead of "$HOME" strings that only worked inside shell commands, and resource_path() no longer shells out to find.
  • AltStore feed download hardened: matches by bundleIdentifier (the feed literally has two apps both named "AltStore" now), skips the Patreon entries that don't link to a direct .ipa, and has timeouts and error handling.
  • Small stuff: don't start a second anisette-server when one is already answering on port 6969, a makedirs crash on second run, and the Launch at Login checkbox getting passed a widget instead of True.
  • Added a small unittest file for the helpers.

One thing I could not fix and want to flag for whoever comes next: on iOS 26 the sideload itself succeeds but AltStore crashes the moment you open it. That one is not althea's fault. iOS 26 got stricter about signature validation, and the official AltServer fixed it in 1.7.4 by updating their codesigning library, but AltServer-Linux (the binary althea downloads) is still v0.0.5 from 2022 and has not gotten that fix (NyaMisty/AltServer-Linux#131). There's a fork experimenting with routing the signing through rcodesign, but no released binary with it yet, so I left that alone rather than ship something I couldn't verify. Documenting it here so the next person doesn't spend a weekend confused like I did.

Thanks for making althea 🙏

resource_path() shelled out to 'find' to detect an installed copy and fell
back to the current working directory, which broke when althea was launched
from anywhere but the repo root (and left dead code after the return). It now
uses os.path against the script's real location.

The AltServer/anisette-server/AltStore paths were literal "$HOME/..." strings
that only worked because they were later interpolated into shell commands.
They are now real paths derived from altheapath (honoring XDG_DATA_HOME), and
the data directory is created with makedirs(exist_ok=True).

AI-assisted written with Claude Code.
iOS versions were compared as plain strings (ios_version() >= "15.0"), which
breaks for double-digit majors, and version.parse() crashed with
InvalidVersion when no device was connected because ideviceinfo returned
nothing and the fallback string "result" was parsed as a version.

Add parse_ios_version(), which returns None in that case, and show a clear
"connect and unlock your device" dialog instead of crashing. The failure
dialog is dispatched to the GTK main thread via GLib.idle_add rather than
being created from the worker thread. The update check in notify() gets the
same numeric comparison plus a timeout and error handling.

AI-assisted written with Claude Code.
AltServer was started through an f-string shell command with the Apple ID,
password, and IPA path interpolated straight in, so a password containing a
space, quote, or any shell metacharacter broke the command (or worse). The
log redirection also used a literal $HOME path that could disagree with the
XDG-aware path the monitor reads.

Launch it with an argv list and env instead: no shell involved, stderr merged
into the same log file the monitor reads, and the anisette server address
passed via the environment. When coreutils' stdbuf is available it is used to
line-buffer output so 2FA/warning prompts appear in the log promptly; when it
is not, the launch still works.

AI-assisted written with Claude Code.
install_process() ran a while-loop inside a GLib.idle_add callback, spawning
four grep subprocesses per iteration against log.txt and never yielding, so
the entire UI froze for the duration of an install and the 2FA/warning
dialogs raced the loop. It also wrote prompt replies with communicate(),
which closes stdin after the first reply.

Rewrite it as a GLib.timeout_add(200) callback: read the log in-process via
a small read_install_log() helper (which also copes with the file not
existing yet), check the markers with plain substring tests, and return
True/False to keep or stop the poll. Prompt replies now go through
stdin.write()+flush() so the pipe stays open for a second prompt, and the
warn/2FA one-shot flags live on the window instead of loop locals.

AI-assisted written with Claude Code.
Three small ones found while debugging the install flow:

- The splash screen probed port 6969 to see whether anisette-server was
  already running but then unconditionally started another instance anyway;
  only start one when the probe fails.
- os.makedirs(.../lib/x86_64) crashed with FileExistsError on any second run
  that re-downloaded anisette-server; pass exist_ok=True.
- command_six.set_active(command_six) passed the widget itself instead of
  True for the Launch at Login checkbox.

AI-assisted written with Claude Code.
The AltStore feed lists two apps both named "AltStore" (the stable app and a
separate beta under com.rileytestut.AltStore.Beta), and some version entries
link to Patreon rather than a direct .ipa. The old code matched by name and
blindly took versions[0], so it could pick the wrong app or a non-.ipa URL
depending on feed ordering, and had no timeouts or error handling.

- Match feed entries by bundleIdentifier instead of name.
- Skip version entries whose downloadURL isn't a direct .ipa.
- Add network timeouts and error handling around the feed and IPA download,
  and use os.replace for an atomic swap into place.

AI-assisted written with Claude Code.
Cover resource_path() outside the repo tree, the XDG-derived runtime paths,
read_install_log() with a missing and a populated log, and
parse_ios_version() for the no-device and real-version cases. Also ignore
__pycache__ and the local CLAUDE.md notes file.

AI-assisted written with Claude Code.
@logikodied
logikodied force-pushed the fix/install-monitoring-stability branch from 9fc11e9 to 76f1277 Compare July 14, 2026 20:03
@logikodied

Copy link
Copy Markdown
Author

Small update since opening this: I reworked the branch so every fix is its own commit instead of two big ones, which should make it much easier to review (or drop anything you don't want). Only actual code change on top of what was here before: the AltServer launch no longer hard-depends on coreutils' stdbuf. If stdbuf is on the system it's used to line-buffer the log output, and if it isn't, the install still works instead of failing with FileNotFoundError. Force-pushed, so the old commits got rewritten.

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.

1 participant