Commit Graph
8 Commits
Author SHA1 Message Date
esefxdzandClaude Opus 5 e7ef251882 fix(runtime): keep Xray's startup output so a failed core can be diagnosed
The monitor thread read the core's merged stdout/stderr and dropped
every line:

    process.inputStream.bufferedReader().use { reader ->
        reader.forEachLine { _ -> }
    }

so the only trace of a core that died at startup was "Xray process
exited with code 23", with nothing to say why. That exit is real and
intermittent - it showed up on both a handset and an emulator - and
was undiagnosable in the field.

Keep at most 40 lines, and only from the core's first 10 seconds, then
log them when the exit code is non-zero. A core up longer than that may
be carrying traffic and its log can name destinations, which must never
reach the device log (rule 95), so past the window the buffer is
dropped rather than kept.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 15:37:29 +03:00
esefxdzandClaude Opus 5 4e5c7a02db fix(runtime): make Xray core recovery work, and stop it leaking a core per try
Core recovery could never succeed, and every attempt left a live
libxray.so behind.

startCore() calls startTimer(), which builds a CountDownTimer and so a
Handler on the calling thread. handleXrayCoreExit() runs recovery on a
bare Thread, so the constructor threw "Can't create handler inside
thread ... that has not called Looper.prepare()" and took the whole
restart down with it. The catch then deleted the ephemeral config and
returned false without killing the process pb.start() had already
started, so each retry stranded another core.

Measured on an emulator by killing the live core:

  before  1 -> 5 -> 6 -> 8 processes over 120 s, never recovers
  after   "Xray core recovered without dropping TUN" on attempt 1,
          exactly 1 process

startTimer now posts to the main looper, re-checking RUNTIME_TOKEN
inside the post so a stop that lands while it is queued cannot leave a
timer running for a dead generation. stopTimer posts too, so the
countDownTimer field is only touched from one thread. The catch clears
xrayProcess before destroying it, so the monitor thread's
`xrayProcess === process` guard fails and recovery is not re-entered
for a core being killed here.

Pre-existing on main; reachable whenever the core exits.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 15:37:29 +03:00
esefxdzandClaude Opus 5 30d96a4049 style: cut the comments down
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 00:50:36 +03:00
esefxdzandClaude Opus 5 8ba3b6c543 fix(session): alert on every unsolicited native stop, not just the deadline
Second review pass found that VpnStatus.error is unreachable mid-session:
AppConfigs.V2RAY_STATES has no error state and the plugin's stateName when()
ends in else -> "DISCONNECTED", so Dart's unknown -> error mapping is dead
code on Android. Every native stop therefore arrived as a plain disconnect,
and anything other than the expiry deadline - an Xray or TUN setup failure, a
mid-session fail-closed deadline arm - left the user on their real IP with no
notification and no message. That is the exact hole this feature exists to
close.

stopAll() is the single funnel for every native stop, so the alert moves
there: it fires when the stop interrupted a live tunnel and the user did not
ask for it. isRunning is captured before cleanup() clears it, which keeps a
failed connect silent - it never had a tunnel to interrupt. STOP_SERVICE
(the notification's Disconnect action) and onRevoke pass userRequested: in
the revoke case the user switched VPNs or withdrew consent themselves, and
may well be protected by the app that took over.

Also from the pass:
- re-arming now needs the deadline to move by more than 15 s, so round-trip
  jitter cannot cancel and re-post a warning that is already on screen
- a warning delivered with under a minute left is dropped rather than
  announcing "5 minutes"; the disconnected alert follows anyway
- the _doDisconnect comment no longer claims endedMessage posts the alert
- CHANGELOG: the alert does now cover the tunnel stopping unexpectedly

flutter analyze clean; debug APK builds with the Kotlin recompiled.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 00:44:51 +03:00
esefxdzandClaude Opus 5 dd1082c8de fix(session): repair the expiry warning and the gaps the review found
Review of 742f376 (Fable). Three defects made the feature not work for the
case it exists for:

- armExpiryWarning() cancelled the alarm before its own early return. Dart
  re-pushes the deadline every 60 s with no dedupe (rule 93's ">5 s" dedupe
  does not exist), so the first sync after the warning came due deleted the
  armed alarm and never re-armed it. Cancel now happens only on the branch
  that re-arms.
- cancelSessionWarningAlarm() built its PendingIntent with
  FLAG_UPDATE_CURRENT, rewriting the live alarm's RUNTIME_TOKEN to "" so a
  delivery in flight was dropped as a stale generation. It now looks the
  existing PendingIntent up with FLAG_NO_CREATE.
- SOCKS5 mode never requested POST_NOTIFICATIONS (requestPermission() is
  TUN-only because it also raises the VPN consent dialog), so on Android 13+
  both alerts were dropped. New plugin method requests it on its own.

Also:
- the native deadline wins the expiry race against the 1 s Dart tick, so the
  commonest session end reached _onVpnConnectionChanged as a bare tunnel
  drop with no message. It now reconciles and sets the expiry text, with
  postAlert false since the native side already posted.
- a resume reconnect no longer resets the support-ad claim: it re-attaches
  to the same server session, where the top-up has already been spent.
- RevoltAlerts sets setOnlyAlertOnce so a replacing post does not re-sound.
- AGENTS.md rule 93 described a setSessionExpiry/SESSION_EXPIRES_AT_MS path
  that no longer exists, dedupe included. Corrected; that stale text is what
  the warning bug was written against.
- CHANGELOG no longer claims the alert covers the tunnel stopping on its own.

flutter analyze clean; debug APK builds.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 00:22:26 +03:00
esefxdzandClaude Opus 5 742f37683e feat(session): warn before a session ends and say when protection stops
Review findings #1-#3 in internal/APP_REVIEW_2026-10-16.md: every session
end dropped the user onto their real IP with no warning, and the app showed
no errors at all.

- RevoltAlerts (vendored runtime) owns both alerts: a high-importance
  channel, ids 2 and 3, posted from whichever process is alive
- XrayVPNService schedules a 5-minute warning off the deadline it already
  holds, best effort (a warning that cannot be armed never stops a tunnel),
  with its own request code so it cannot clobber the expiry alarm
- the warning intent is handled above the command dispatch: without that it
  would fall through to the final else and stop the tunnel 5 minutes early
- SessionTimer._doDisconnect takes endedMessage: set for ends the user did
  not ask for, which both posts the alert and gives the UI a sentence
- the API being unreachable no longer disconnects a working tunnel; it is a
  display state now, with expiry still enforced natively and server-side
- status_bar.dart renders connect errors, connecting progress and the
  session-end reason

flutter analyze clean; debug APK builds (Kotlin compiles).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 00:02:20 +03:00
esefxdz 6d8a923475 notification fixed 2026-09-06 13:07:37 +03:00
Naveax 5f851f3485 build: vendor pinned flutter_vless Android runtime (#10)
Replace build-time pub-cache mutation with a repository-owned pinned runtime so reviewed native source is the source compiled into the app.
2026-09-06 11:23:48 +03:00