Skip to content

Ask iOS not to suspend the app while a shell is open (#46) - #47

Open
m96-chan wants to merge 3 commits into
mainfrom
background-assertion
Open

m96-chan wants to merge 3 commits into
mainfrom
background-assertion

Conversation

@m96-chan

@m96-chan m96-chan commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Locking the screen or leaving the app drops the SSH session.

The app is not the one hanging up

enterBackground() keeps the shell, the parser state and the credentials, and on return checkRetainedConnection reuses the same PTY when the transport answers. The session dies because iOS suspends the process, and a suspended process cannot service its socket.

The app was not asking for any time before that happened. There is no beginBackgroundTask anywhere in it, so the system is free to suspend the moment the scene goes to the background.

What this buys

An assertion worth about thirty seconds, which covers the cases most likely to be behind the report:

  • glancing at another app and coming straight back
  • pressing the power button and unlocking a few seconds later
  • pulling down a notification

Those survive today only by luck. With the assertion held, the probe on return finds a live transport rather than a dead one.

Taken only when a session is connected or checking. Asking the system to stay awake with nothing to keep alive is wasted battery and the kind of thing that gets noticed.

What this is not

This does not keep the app connected in the background, and is not meant to. No entitlement iOS grants an SSH client holds a TCP session open indefinitely — the categories are audio, location, VoIP, external accessory and NEAppPushProvider, and a terminal is none of them. README.md already points at a remote multiplexer for continuity across a real disconnection, and #39 tracks the one legitimate route to not being suspended at all.

Thirty seconds is a real improvement to the common case and nothing more. It is worth being plain about which one the report was about.

Tests

The assertion is injectable because the failure worth testing is not taking one — it is failing to give one back, which is how iOS kills an app.

test covers
nothingIsAskedForWhenNoSessionIsConnected no assertion with nothing to keep alive
theAssertionIsGivenBackWhenTheAppReturns ended on foreground
theAssertionIsGivenBackWhenTheTimeRunsOut ended from the expiry handler, and not ended twice afterwards
goingToTheBackgroundTwiceHoldsOneAssertion no leak on a repeated transition

All four pass. Simulators shut down after the run.

🤖 Generated with Claude Code

Locking the screen or leaving the app drops the SSH session. Nothing in the app
hangs up — `enterBackground()` keeps the shell, the parser state and the
credentials, and `checkRetainedConnection` reuses the same PTY on return when it
answers. The session dies because iOS suspends the process, and a suspended
process cannot service its socket.

The app was not asking for any time before that happened. There is no
`beginBackgroundTask` in it, so the system is free to suspend the moment the
scene goes to the background. It now holds an assertion, which is worth about
thirty seconds and covers the cases most likely to be behind the report:

  - glancing at another app and coming straight back
  - pressing the power button and unlocking a few seconds later
  - pulling down a notification

Those survive today only by luck. With the assertion held, the probe on return
finds a live transport instead of a dead one.

Taken only when a session is connected or being checked. Asking the system to
stay awake with nothing to keep alive is wasted battery and the kind of thing
that gets noticed.

This does not make the app stay connected in the background, and is not meant
to. No entitlement iOS grants an SSH client holds a TCP session open
indefinitely — the categories are audio, location, VoIP, external accessory and
`NEAppPushProvider`, and a terminal is none of them. `README.md` already points
at a remote multiplexer for continuity across a real disconnection, and #39 is
where the one legitimate route to not being suspended is tracked.

The assertion is injectable because the failure worth testing is not taking one
— it is failing to give one back, which is how iOS kills an app. Four tests
cover the paths that matter: not asking with nothing connected, giving it back
on return, giving it back from the expiry handler, and not double-ending it
afterwards.

Co-Authored-By: Claude Opus 5 (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.

1 participant