Repository navigation
Kindness mode: stop the stopped proxy, survive being killed, clean up UPnP - #1807
Conversation
…s restarts guardianproject#1183 guardianproject#1795 enableProxy() did its real work in an unsynchronized coroutine, so a stop arriving before that coroutine ran was a no-op and the proxy it then started belonged to nobody. The service stops and restarts the proxy on every network event, which made that window easy to hit. Starts now carry a generation number and back off if a stop or newer start won the race. IPtProxy has the same shape internally (start() flips isRunning inside its own goroutine, stop() is a no-op until then), so a stop also watches for a couple of seconds and puts down a proxy that came up late. Mapped UPnP ports were only tracked in memory, so a killed process left them open on the router until the lease expired. The mapping is written to prefs before the proxy starts and cleared on release; releaseStalePorts() closes anything a dead run left behind.
…t#1799 guardianproject#1783 START_STICKY restarts get throttled and some OEMs never deliver them, so a killed SnowflakeProxyService stayed dead until someone opened the app and noticed. The pref is already the user's standing intent, so honor it: OnBootReceiver starts the service after a reboot when kindness mode is on, and a 15 minute WorkManager watchdog restarts it if it is wanted, allowed in this region, and not running. The watchdog cancels itself when the user turns kindness mode off, and a denied background start is left for the next boot or app open rather than crashing. The service also releases stale UPnP mappings at startup now, closing the loop on what a killed process left mapped.
|
Thanks @meenbeese, that makes sense for the counter reset. A PeriodicWorkRequest is a lot of machinery to zero one integer, and dropping the dependency for that is fair. The watchdog here does a different job: re-arming Kindness Mode after the service gets killed while the user still wants it running. START_STICKY on its own wasn't enough in testing (that's the note in KindnessWatchdogWorker), so it ended up as a Worker. If the plan is to drop androidx.work across the board, what would you want for the periodic re-arm instead, an AlarmManager setExactAndAllowWhileIdle loop? I'll rework this around whatever the project lands on, as long as it keeps surviving a kill. Reading through #1802 now. |
|
The plan is to drop AndroidX Work at the moment. Can you look at JobScheduler or a simple foreground service as replacement options? I didn't really look at the code that much but one of those should work here. |
|
Makes sense. JobScheduler is a clean fit, since that's what WorkManager runs on underneath, so I can drop the androidx.work dependency and keep the same survive-a-kill behavior. One note on the foreground-service option: the watchdog exists to come back after the service is already gone (force-stop, OEM battery killers), and a plain foreground service doesn't get re-armed once it's killed. That's the START_STICKY gap the Worker was covering, so I'll go the JobScheduler route rather than a bare service. I'll rework it onto JobScheduler and push an update here. |
Swaps the Kindness Mode watchdog off androidx.work per feedback on this PR. WorkManager runs on top of JobScheduler anyway, so the actual survive-a-kill behavior doesn't change, but we drop a dependency this one file was pulling in just for a periodic check. The androidx.work gradle dependency itself stays for now since Prefs and ResetSnowflakesServedWeeklyWorker still use it; that's getting removed in a separate PR. shouldRestart() keeps the same signature and logic, just moved onto KindnessWatchdogJob. The job registers in the manifest like any other JobService and gets scheduled/cancelled through JobScheduler directly.
|
Pushed the JobScheduler version (2cc1be6). The watchdog is a JobService now, a 15-minute periodic job with setPersisted so it still comes back after a reboot, and the same shouldRestart logic and tests carried over. I left androidx.work in the build for now, since the weekly snowflake-count reset still uses it. Once #1802 lands and that goes away, the dependency drops out entirely. Compiles clean and the unit tests pass. |
|
This one is exciting, but a bit involved - I'll test and review this soon. If this works as well as you think it does, and touches all of these longstanding issues you referenced above, then this is a very special PR. Thanks again <3 |
- for guardianproject#1795 the possibly stale UPnP ports issue, move the call to clean up lingering ports directly into SnowflakeProxyWrapper's constructor to prevent a possible future regression - also move the call to start the new KindnessWatchdogJob there - various lint fixes - renamed unclear methods, removed superflous helper methods, - add comments for unclear magic return false's and such things
- for guardianproject#1795 the possibly stale UPnP ports issue, move the call to clean up lingering ports directly into SnowflakeProxyWrapper's constructor to prevent a possible future regression - also move the call to start the new KindnessWatchdogJob there - various lint fixes - renamed unclear methods, removed superflous helper methods, - add comments for unclear magic return false's and such things
392bfad to
27516aa
Compare
- for guardianproject#1795 the possibly stale UPnP ports issue, move the call to clean up lingering ports directly into SnowflakeProxyWrapper's constructor to prevent a possible future regression - also move the call to start the new KindnessWatchdogJob there - various lint fixes - renamed unclear methods, removed superflous helper methods, - add comments for unclear magic return false's and such things - add test handling integer overflow
27516aa to
bf6c1f7
Compare
- for guardianproject#1795 the possibly stale UPnP ports issue, move the call to clean up lingering ports directly into SnowflakeProxyWrapper's constructor to prevent a possible future regression - also move the call to start the new KindnessWatchdogJob there - various lint fixes - renamed unclear methods, removed superflous helper methods, - add comments for unclear magic return false's and such things - add test handling integer overflow - add logs, especially to KindnessWatchdogJob
…esnt need to exist in its own file
- remove unhelpful and cluttering log from recent work in PreferenceProvider.kt
bf6c1f7 to
3afb29a
Compare
|
This is great work. I extended it a bit, merged it to It's easy to see that it fixes:
I am inclined to agree with your theory about #1183 above, but I don't feel comfortable closing that issue yet. Until we get a lot more testing and various other people measuring and observing their CPU usage. |

Kindness mode has a habit of quietly not being on anymore. This traces the three ways that happens and closes each of them.
The stopped proxy that keeps running (#1183).
enableProxy()takes its lock, then does the real work in a coroutine that has no lock at all. AstopProxy()that lands before that coroutine runs seesproxy == null, does nothing, and the coroutine then starts a proxy nothing owns. The service stops and restarts the proxy on every network callback, so this window gets hit in normal use, and an orphaned proxy is a plausible source of the CPU drain reported there. Starts now carry a generation number: if a stop or a newer start won while the ports were being mapped, the late start closes its own mappings and never callsstart().There is a second copy of the same race one layer down. IPtProxy's
Start()flips itsisRunningflag inside a goroutine it spawns, and itsStop()is a no-op until that flag is set, so a fast stop is silently ignored and the proxy comes up anyway:That matches cohosh's shutdown race hypothesis in the issue. The app cannot fix the library, but it can stop dropping its reference too early: after
stop(), the wrapper watches the instance for two seconds and stops it again if it came up late. The remainder, if any, is upstream in IPtProxy.The service that stays dead (#1799, #1783). START_STICKY is the only thing that brought the service back, and sticky restarts get throttled, abandoned after repeated kills, and never delivered at all on some builds. The only re-arm paths were opening the app or toggling the switch, which is why people find the proxy off hours later. The pref is already the user's standing intent, so it gets honored now:
OnBootReceiverstarts the service after a reboot when kindness mode is on, and a 15 minute WorkManager watchdog (the dependency was already in the app) restarts it when it is wanted, allowed in this region, and not running. The watchdog cancels itself when the user turns kindness mode off. A background start denial on 31+ is caught and left for the next boot or app open. This will not resurrect the service inside a stopped GrapheneOS work profile, nothing can, but within a running profile the watchdog covers the kill-and-forget case.The port mappings that outlive the process (#1795). Mapped UPnP ports lived in one in-memory list, so any abnormal termination left them open on the router until the lease expired, with nothing on the next launch knowing they existed. The mapping is written to prefs before the proxy starts, cleared on clean release, and
releaseStalePorts()closes whatever a dead run left behind on the next service start.Fixes #1183, fixes #1799, fixes #1795. Helps #1783 within what the platform allows.
Compatible with #1805: this deliberately stays out of
initNetworkCallbacks(), so both apply cleanly in either order.Verified with the unit suite (eleven new tests: the start-commit and watchdog decision tables, and the port record parsing) and
assembleFullpermDebug, both clean. I do not have a device farm, so the multi-hour soak that #1799 describes is exactly the test this needs from someone who does. Happy to adjust anything.One thing I noticed but left alone, since it is outside these issues: the preference listener in
SnowflakeProxyService.onCreatereturns early unless a key equals two different constants at once, so the region kill switch and the camo refresh there look unreachable. Can file separately if useful.