From be0c5ed4d33e1deac871d4835bb94d5decd3436c Mon Sep 17 00:00:00 2001 From: dr mike Date: Mon, 29 Jun 2026 02:49:04 +0330 Subject: [PATCH] Fix Android connect crash using Java ProcessBuilder for Xray (v1.1.7). Replace System.Diagnostics.Process on Android with a native process host so Connect no longer force-closes on Samsung and similar devices; marshal mobile UI updates to the main thread and surface startup errors in-app. --- CHANGELOG.md | 9 + docs/releases/v1.1.7.md | 44 +++++ src/v2rayF.Android/Application.cs | 3 + .../Services/AndroidCoreEnvironment.cs | 2 + .../Services/AndroidJavaCoreProcessHost.cs | 142 ++++++++++++++++ src/v2rayF.Core/Services/AppServices.cs | 2 + src/v2rayF.Core/Services/ICoreEnvironment.cs | 2 + src/v2rayF.Core/Services/ICoreProcessHost.cs | 21 +++ src/v2rayF.Core/Services/LatencyService.cs | 77 ++------- .../Services/ManagedCoreProcessHost.cs | 134 +++++++++++++++ src/v2rayF.Core/Services/ProxyCoreService.cs | 156 ++---------------- .../Services/DesktopCoreEnvironment.cs | 2 + src/v2rayF/ViewModels/MainWindowViewModel.cs | 64 ++++--- 13 files changed, 431 insertions(+), 227 deletions(-) create mode 100644 docs/releases/v1.1.7.md create mode 100644 src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs create mode 100644 src/v2rayF.Core/Services/ICoreProcessHost.cs create mode 100644 src/v2rayF.Core/Services/ManagedCoreProcessHost.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0c1ce..ee28a22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to v2rayF are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.7] - 2026-06-28 + +### Fixed + +- Android connect crash — Xray now starts via Java `ProcessBuilder` instead of `System.Diagnostics.Process` (fixes force-close on Samsung and other devices) +- Android connect — proxy core resolves the process host at runtime so the Android host is always used +- Android connect — status and busy state updates are marshalled to the UI thread after every async step +- Latency tests on Android use the same Java process launcher (no .NET `Process` on mobile) + ## [1.1.6] - 2026-06-28 ### Fixed diff --git a/docs/releases/v1.1.7.md b/docs/releases/v1.1.7.md new file mode 100644 index 0000000..25b973d --- /dev/null +++ b/docs/releases/v1.1.7.md @@ -0,0 +1,44 @@ +# v2rayF v1.1.7 + +Critical Android connect crash fix for Samsung Galaxy M21 and similar devices. + +--- + +## Highlights + +- Fixes app force-close when tapping **Connect** on Android 12+ +- Xray core now starts through Android's native `ProcessBuilder` (not .NET `Process`) +- Connection errors show in the app instead of silently crashing + +--- + +## Downloads + +| Platform | Package | How to run | +|----------|---------|------------| +| Android ARM64 | `v2rayF-android-arm64.zip` | Install `v2rayF-android-arm64.apk` | + +Uninstall v1.1.6 first if Connect still crashes, then install this APK. + +--- + +## Fixed + +- Connect crash from `System.Diagnostics.Process.Start` on Android (Galaxy M21, Android 12) +- Wrong process host used if core service initialized before Android app setup +- UI property updates after `await` running off the main thread on mobile +- Speedtest/latency on Android using the same unsafe .NET process API + +--- + +## Safe connect (poor networks) + +1. **Check** — Xray starts locally without VPN (your normal internet is untouched) +2. **VPN** — only enabled after the core is verified +3. **Fail** — VPN is removed immediately if anything goes wrong; you see an error message + +--- + +## Full changelog + +[CHANGELOG.md](https://github.com/drmikecrypto/v2rayF/blob/main/CHANGELOG.md) diff --git a/src/v2rayF.Android/Application.cs b/src/v2rayF.Android/Application.cs index c199ebd..e2d7e1f 100644 --- a/src/v2rayF.Android/Application.cs +++ b/src/v2rayF.Android/Application.cs @@ -21,6 +21,9 @@ public override void OnCreate() { AppServices.CoreEnvironment = new AndroidCoreEnvironment(); AppServices.Platform = new AndroidPlatformIntegration(); + AppServices.CoreProcessHost = new AndroidJavaCoreProcessHost(); + AndroidEnvironment.UnhandledExceptionRaiser += (_, e) => + global::Android.Util.Log.Error("v2rayF", e.Exception?.ToString() ?? "Unhandled exception"); base.OnCreate(); _ = WarmupAsync(); } diff --git a/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs b/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs index 6959cc0..f95e5e1 100644 --- a/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs +++ b/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs @@ -63,4 +63,6 @@ private static async Task ExtractAssetIfMissingAsync(string assetName, string de await using var output = File.Create(destPath); await input.CopyToAsync(output, cancellationToken).ConfigureAwait(false); } + + public ICoreProcessHost CreateProcessHost() => new AndroidJavaCoreProcessHost(); } diff --git a/src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs b/src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs new file mode 100644 index 0000000..e40443e --- /dev/null +++ b/src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs @@ -0,0 +1,142 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Java.IO; +using Java.Lang; +using v2rayF.Services; +using Process = Java.Lang.Process; + +namespace v2rayF.Android.Services; + +/// +/// Starts Xray via Android ProcessBuilder — System.Diagnostics.Process crashes on many devices. +/// +public sealed class AndroidJavaCoreProcessHost : ICoreProcessHost +{ + private readonly object _lock = new(); + private Process? _process; + private string _recentOutput = ""; + + public bool IsRunning + { + get + { + lock (_lock) + return _process is { IsAlive: true }; + } + } + + public bool HasExited + { + get + { + lock (_lock) + return _process is null || !_process.IsAlive; + } + } + + public Task StartAsync( + string corePath, + string configPath, + string workingDirectory, + CancellationToken cancellationToken = default) + { + StopAsync(cancellationToken).GetAwaiter().GetResult(); + + lock (_lock) + _recentOutput = ""; + + var cmd = new[] { corePath, "run", "-c", configPath }; + var builder = new ProcessBuilder(cmd); + builder.Directory(new File(workingDirectory)); + builder.RedirectErrorStream(true); + + Process process; + lock (_lock) + { + _process = builder.Start(); + process = _process; + } + + if (process is not null) + _ = Task.Run(() => DrainOutputAsync(process), cancellationToken); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken = default) + { + lock (_lock) + { + if (_process is null) + return Task.CompletedTask; + + try + { + if (_process.IsAlive) + _process.Destroy(); + } + catch + { + // Best effort shutdown. + } + finally + { + _process = null; + } + } + + return Task.CompletedTask; + } + + public string GetRecentError() + { + lock (_lock) + { + if (_process is { IsAlive: true }) + return _recentOutput.Trim(); + + if (_process is not null) + { + try + { + var code = _process.ExitValue(); + var output = _recentOutput.Trim(); + return string.IsNullOrEmpty(output) + ? $"Xray exited with code {code}" + : output; + } + catch (IllegalThreadStateException) + { + return _recentOutput.Trim(); + } + } + + return _recentOutput.Trim(); + } + } + + private void DrainOutputAsync(Process process) + { + try + { + using var reader = new BufferedReader(new InputStreamReader(process.InputStream)); + string? line; + while ((line = reader.ReadLine()) is not null) + { + lock (_lock) + { + if (_recentOutput.Length > 0) + _recentOutput += '\n'; + _recentOutput += line; + if (_recentOutput.Length > 8192) + _recentOutput = _recentOutput[^4096..]; + } + } + } + catch + { + // Process ended. + } + } +} diff --git a/src/v2rayF.Core/Services/AppServices.cs b/src/v2rayF.Core/Services/AppServices.cs index 1385173..cac3c50 100644 --- a/src/v2rayF.Core/Services/AppServices.cs +++ b/src/v2rayF.Core/Services/AppServices.cs @@ -9,6 +9,8 @@ public static class AppServices public static IPlatformIntegration Platform { get; set; } = null!; + public static ICoreProcessHost CoreProcessHost { get; set; } = new ManagedCoreProcessHost(); + /// Called when the Android activity stops — tear down VPN so network is not left hijacked. public static Func? EmergencyDisconnectAsync { get; set; } } diff --git a/src/v2rayF.Core/Services/ICoreEnvironment.cs b/src/v2rayF.Core/Services/ICoreEnvironment.cs index 97ae6c7..dffbe84 100644 --- a/src/v2rayF.Core/Services/ICoreEnvironment.cs +++ b/src/v2rayF.Core/Services/ICoreEnvironment.cs @@ -12,4 +12,6 @@ public interface ICoreEnvironment string GetCoresDirectory(); string GetDataDirectory(); + + ICoreProcessHost CreateProcessHost(); } diff --git a/src/v2rayF.Core/Services/ICoreProcessHost.cs b/src/v2rayF.Core/Services/ICoreProcessHost.cs new file mode 100644 index 0000000..3227923 --- /dev/null +++ b/src/v2rayF.Core/Services/ICoreProcessHost.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace v2rayF.Services; + +public interface ICoreProcessHost +{ + bool IsRunning { get; } + + bool HasExited { get; } + + Task StartAsync( + string corePath, + string configPath, + string workingDirectory, + CancellationToken cancellationToken = default); + + Task StopAsync(CancellationToken cancellationToken = default); + + string GetRecentError(); +} diff --git a/src/v2rayF.Core/Services/LatencyService.cs b/src/v2rayF.Core/Services/LatencyService.cs index 98d4cc5..0e9ef36 100644 --- a/src/v2rayF.Core/Services/LatencyService.cs +++ b/src/v2rayF.Core/Services/LatencyService.cs @@ -19,6 +19,7 @@ public sealed class LatencyService ]; private readonly ICoreEnvironment _environment; + private readonly ICoreProcessHost _speedtestHost; private readonly SemaphoreSlim _speedtestLock = new(1, 1); public const int TimeoutMs = 10000; @@ -26,6 +27,7 @@ public sealed class LatencyService public LatencyService(ICoreEnvironment environment) { _environment = environment; + _speedtestHost = environment.CreateProcessHost(); } public async Task MeasureAsync(ProxyServer server, CancellationToken cancellationToken = default) @@ -52,7 +54,6 @@ public LatencyService(ICoreEnvironment environment) private async Task MeasureViaCoreAsync(ProxyServer server, CancellationToken cancellationToken) { await _speedtestLock.WaitAsync(cancellationToken).ConfigureAwait(false); - Process? process = null; try { var configDir = Path.Combine(_environment.GetDataDirectory(), "runtime"); @@ -63,12 +64,18 @@ await File.WriteAllTextAsync( XrayConfigBuilder.BuildSpeedtest(server), cancellationToken).ConfigureAwait(false); - process = StartCore(configPath); - if (process is null) + var corePath = _environment.GetCorePath(); + if (!File.Exists(corePath)) return null; - await WaitForCoreReadyAsync(process, cancellationToken).ConfigureAwait(false); - if (process.HasExited) + await _speedtestHost.StartAsync( + corePath, + configPath, + _environment.GetCoresDirectory(), + cancellationToken).ConfigureAwait(false); + + await WaitForCoreReadyAsync(cancellationToken).ConfigureAwait(false); + if (_speedtestHost.HasExited) return -1; return await ProbeThroughSocksAsync(XrayConfigBuilder.SpeedtestSocksPort, cancellationToken) @@ -76,39 +83,16 @@ await File.WriteAllTextAsync( } finally { - await StopCoreAsync(process).ConfigureAwait(false); + await _speedtestHost.StopAsync(cancellationToken).ConfigureAwait(false); _speedtestLock.Release(); } } - private static Process? StartCore(string configPath) - { - var corePath = AppServices.CoreEnvironment.GetCorePath(); - if (!File.Exists(corePath)) - return null; - - var process = CoreProcessLauncher.CreateProcess( - corePath, - configPath, - AppServices.CoreEnvironment.GetCoresDirectory()); - - try - { - CoreProcessLauncher.Start(process); - return process; - } - catch - { - process.Dispose(); - return null; - } - } - - private static async Task WaitForCoreReadyAsync(Process process, CancellationToken cancellationToken) + private async Task WaitForCoreReadyAsync(CancellationToken cancellationToken) { for (var i = 0; i < 20; i++) { - if (process.HasExited) + if (_speedtestHost.HasExited) return; if (await IsPortOpenAsync("127.0.0.1", XrayConfigBuilder.SpeedtestSocksPort, cancellationToken) @@ -137,37 +121,6 @@ private static async Task IsPortOpenAsync(string host, int port, Cancellat } } - private static async Task StopCoreAsync(Process? process) - { - if (process is null) - return; - - try - { - if (!process.HasExited) - { - CoreProcessLauncher.Kill(process); - using var timeout = new CancellationTokenSource(2000); - try - { - await process.WaitForExitAsync(timeout.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Timed out waiting for exit. - } - } - } - catch - { - // Best effort shutdown. - } - finally - { - process.Dispose(); - } - } - private static async Task ProbeThroughSocksAsync(int socksPort, CancellationToken cancellationToken) { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); diff --git a/src/v2rayF.Core/Services/ManagedCoreProcessHost.cs b/src/v2rayF.Core/Services/ManagedCoreProcessHost.cs new file mode 100644 index 0000000..34ca9c1 --- /dev/null +++ b/src/v2rayF.Core/Services/ManagedCoreProcessHost.cs @@ -0,0 +1,134 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace v2rayF.Services; + +public sealed class ManagedCoreProcessHost : ICoreProcessHost +{ + private readonly object _stderrLock = new(); + private readonly StringBuilder _recentStderr = new(); + private Process? _process; + private bool _manualStop; + + public bool IsRunning => _process is { HasExited: false }; + + public bool HasExited => _process is null || _process.HasExited; + + public Task StartAsync( + string corePath, + string configPath, + string workingDirectory, + CancellationToken cancellationToken = default) + { + StopAsync(cancellationToken).GetAwaiter().GetResult(); + + lock (_stderrLock) + { + _recentStderr.Clear(); + } + + _process = CoreProcessLauncher.CreateProcess(corePath, configPath, workingDirectory); + _process.ErrorDataReceived += OnErrorDataReceived; + _process.Exited += OnProcessExited; + _manualStop = false; + + CoreProcessLauncher.Start(_process, line => + { + lock (_stderrLock) + { + if (_recentStderr.Length > 0) + _recentStderr.AppendLine(); + _recentStderr.Append(line); + } + }); + + _ = DrainOutputAsync(_process); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + var process = Interlocked.Exchange(ref _process, null); + if (process is null) + return; + + _manualStop = true; + try + { + if (!process.HasExited) + { + CoreProcessLauncher.Kill(process); + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(3000); + try + { + await process.WaitForExitAsync(timeout.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Timed out waiting for exit. + } + } + } + catch + { + // Best effort shutdown. + } + finally + { + process.ErrorDataReceived -= OnErrorDataReceived; + process.Exited -= OnProcessExited; + process.Dispose(); + _manualStop = false; + } + } + + public string GetRecentError() + { + lock (_stderrLock) + { + return _recentStderr.ToString().Trim(); + } + } + + private void OnProcessExited(object? sender, EventArgs e) + { + if (_manualStop) + return; + + _process = null; + } + + private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (string.IsNullOrEmpty(e.Data)) + return; + + lock (_stderrLock) + { + if (_recentStderr.Length > 0) + _recentStderr.AppendLine(); + _recentStderr.Append(e.Data); + } + } + + private static async Task DrainOutputAsync(Process process) + { + try + { + while (!process.HasExited) + { + var line = await process.StandardOutput.ReadLineAsync().ConfigureAwait(false); + if (line is null) + break; + } + } + catch + { + // Process ended. + } + } +} diff --git a/src/v2rayF.Core/Services/ProxyCoreService.cs b/src/v2rayF.Core/Services/ProxyCoreService.cs index 6dc00e1..764641a 100644 --- a/src/v2rayF.Core/Services/ProxyCoreService.cs +++ b/src/v2rayF.Core/Services/ProxyCoreService.cs @@ -1,8 +1,6 @@ using System; -using System.Diagnostics; using System.IO; using System.Net.Sockets; -using System.Text; using System.Threading; using System.Threading.Tasks; using v2rayF.Models; @@ -12,18 +10,16 @@ namespace v2rayF.Services; public sealed class ProxyCoreService : IAsyncDisposable { private readonly ICoreEnvironment _environment; - private readonly object _stderrLock = new(); - private readonly StringBuilder _recentStderr = new(); - private Process? _process; private string? _configPath; - private bool _manualStop; + + private static ICoreProcessHost ProcessHost => AppServices.CoreProcessHost; public ProxyCoreService(ICoreEnvironment environment) { _environment = environment; } - public bool IsRunning => _process is { HasExited: false }; + public bool IsRunning => ProcessHost.IsRunning; public ProxyServer? ActiveServer { get; private set; } @@ -66,42 +62,21 @@ public async Task StartAsync(ProxyServer server, AppSettings settings, int? tunF _configPath = Path.Combine(configDir, "config.json"); await File.WriteAllTextAsync(_configPath, configJson, cancellationToken).ConfigureAwait(false); - lock (_stderrLock) - { - _recentStderr.Clear(); - } + await ProcessHost.StartAsync( + ResolveCorePath(), + _configPath, + ResolveCoresDirectory(), + cancellationToken).ConfigureAwait(false); - var corePath = ResolveCorePath(); - _process = CoreProcessLauncher.CreateProcess(corePath, _configPath, ResolveCoresDirectory()); + await WaitForCoreReadyAsync(cancellationToken).ConfigureAwait(false); - if (!CoreProcessLauncher.IsAndroid) + if (ProcessHost.HasExited) { - _process.ErrorDataReceived += OnErrorDataReceived; - _process.Exited += OnProcessExited; - } - - CoreProcessLauncher.Start(_process, line => - { - lock (_stderrLock) - { - if (_recentStderr.Length > 0) - _recentStderr.AppendLine(); - _recentStderr.Append(line); - } - }); - - if (!CoreProcessLauncher.IsAndroid) - _ = DrainOutputAsync(_process); - - await WaitForCoreReadyAsync(_process, cancellationToken).ConfigureAwait(false); - - if (_process.HasExited) - { - var error = GetRecentStderr(); - CleanupProcess(notify: false); + var error = ProcessHost.GetRecentError(); + await StopAsync(cancellationToken).ConfigureAwait(false); throw new InvalidOperationException( string.IsNullOrWhiteSpace(error) - ? CoreProcessLauncher.FormatAndroidStartFailure() + ? "Xray core exited immediately after start." : FormatStartupError(error)); } @@ -111,75 +86,18 @@ public async Task StartAsync(ProxyServer server, AppSettings settings, int? tunF public async Task StopAsync(CancellationToken cancellationToken = default) { - var process = Interlocked.Exchange(ref _process, null); - if (process is null) - return; - - _manualStop = true; - try - { - if (!process.HasExited) - { - CoreProcessLauncher.Kill(process); - using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - timeout.CancelAfter(3000); - try - { - await process.WaitForExitAsync(timeout.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Timed out waiting for exit. - } - } - } - catch - { - // Best effort shutdown. - } - finally - { - if (!CoreProcessLauncher.IsAndroid) - { - process.ErrorDataReceived -= OnErrorDataReceived; - process.Exited -= OnProcessExited; - } - process.Dispose(); - ActiveServer = null; - RunningStateChanged?.Invoke(this, false); - _manualStop = false; - } - } - - public async ValueTask DisposeAsync() => await StopAsync().ConfigureAwait(false); - - private void OnProcessExited(object? sender, EventArgs e) - { - if (_manualStop) - return; - + await ProcessHost.StopAsync(cancellationToken).ConfigureAwait(false); ActiveServer = null; RunningStateChanged?.Invoke(this, false); } - private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) - { - if (string.IsNullOrEmpty(e.Data)) - return; - - lock (_stderrLock) - { - if (_recentStderr.Length > 0) - _recentStderr.AppendLine(); - _recentStderr.Append(e.Data); - } - } + public async ValueTask DisposeAsync() => await StopAsync().ConfigureAwait(false); - private static async Task WaitForCoreReadyAsync(Process process, CancellationToken cancellationToken) + private async Task WaitForCoreReadyAsync(CancellationToken cancellationToken) { for (var i = 0; i < 40; i++) { - if (process.HasExited) + if (ProcessHost.HasExited) return; if (await IsPortOpenAsync("127.0.0.1", XrayConfigBuilder.SocksPort, cancellationToken).ConfigureAwait(false)) @@ -207,46 +125,6 @@ private static async Task IsPortOpenAsync(string host, int port, Cancellat } } - private static async Task DrainOutputAsync(Process process) - { - try - { - while (!process.HasExited) - { - var line = await process.StandardOutput.ReadLineAsync().ConfigureAwait(false); - if (line is null) - break; - } - } - catch - { - // Process ended. - } - } - - private void CleanupProcess(bool notify) - { - if (_process is null) - return; - - _process.ErrorDataReceived -= OnErrorDataReceived; - _process.Exited -= OnProcessExited; - _process.Dispose(); - _process = null; - ActiveServer = null; - - if (notify) - RunningStateChanged?.Invoke(this, false); - } - - private string GetRecentStderr() - { - lock (_stderrLock) - { - return _recentStderr.ToString().Trim(); - } - } - private static string FormatStartupError(string stderr) { if (stderr.Contains("10808", StringComparison.Ordinal) || diff --git a/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs b/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs index 916abc9..5e77aed 100644 --- a/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs +++ b/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs @@ -29,4 +29,6 @@ public string GetDataDirectory() Directory.CreateDirectory(folder); return folder; } + + public ICoreProcessHost CreateProcessHost() => new ManagedCoreProcessHost(); } diff --git a/src/v2rayF/ViewModels/MainWindowViewModel.cs b/src/v2rayF/ViewModels/MainWindowViewModel.cs index 1dc58c3..290dd1c 100644 --- a/src/v2rayF/ViewModels/MainWindowViewModel.cs +++ b/src/v2rayF/ViewModels/MainWindowViewModel.cs @@ -333,19 +333,19 @@ private async Task ConnectMobileAsync() { if (SelectedServer is null) { - StatusText = "Select a server first."; + RunOnUiThread(() => StatusText = "Select a server first."); return; } if (!_proxyCore.IsCoreAvailable() || !_proxyCore.HasGeoFiles()) { - await AppServices.CoreEnvironment.EnsureCoreAsync().ConfigureAwait(true); - UpdateCoreStatus(); + await AppServices.CoreEnvironment.EnsureCoreAsync().ConfigureAwait(false); + RunOnUiThread(UpdateCoreStatus); } if (!_proxyCore.IsCoreAvailable()) { - StatusText = "Xray core not found."; + RunOnUiThread(() => StatusText = "Xray core not found."); return; } @@ -355,29 +355,36 @@ private async Task ConnectMobileAsync() if (settings.RoutingMode == RoutingMode.BypassChina && !_proxyCore.HasGeoFiles()) settings.RoutingMode = RoutingMode.BypassLan; - await _settingsStore.SaveAsync(settings).ConfigureAwait(true); + await _settingsStore.SaveAsync(settings).ConfigureAwait(false); var vpnEngaged = false; try { - IsBusy = true; - StatusText = $"Connecting to {SelectedServer.Name}…"; + RunOnUiThread(() => + { + IsBusy = true; + StatusText = $"Connecting to {SelectedServer.Name}…"; + }); + await ConnectAndroidAsync(SelectedServer, settings, engaged => vpnEngaged = engaged) - .ConfigureAwait(true); + .ConfigureAwait(false); } catch (Exception ex) { - await SafeTeardownAsync(vpnEngaged).ConfigureAwait(true); - StatusText = $"Connection failed: {ex.Message}"; + await SafeTeardownAsync(vpnEngaged).ConfigureAwait(false); + RunOnUiThread(() => StatusText = $"Connection failed: {ex.Message}"); } finally { if (!IsConnected && vpnEngaged) - await SafeTeardownAsync(vpnEngaged: true).ConfigureAwait(true); + await SafeTeardownAsync(vpnEngaged: true).ConfigureAwait(false); - IsBusy = false; - OnPropertyChanged(nameof(ConnectButtonText)); - OnPropertyChanged(nameof(TrayToolTip)); + RunOnUiThread(() => + { + IsBusy = false; + OnPropertyChanged(nameof(ConnectButtonText)); + OnPropertyChanged(nameof(TrayToolTip)); + }); } } @@ -448,7 +455,8 @@ private async Task ConnectAndroidAsync( AppSettings settings, Action markVpnEngaged) { - StatusText = $"Checking {server.Name}…"; + RunOnUiThread(() => StatusText = $"Checking {server.Name}…"); + var probeSettings = new AppSettings { RoutingMode = settings.RoutingMode, @@ -458,24 +466,28 @@ private async Task ConnectAndroidAsync( SubscriptionUrl = settings.SubscriptionUrl }; - await _proxyCore.StartAsync(server, probeSettings, null).ConfigureAwait(true); - await _proxyCore.StopAsync().ConfigureAwait(true); + await _proxyCore.StartAsync(server, probeSettings, null).ConfigureAwait(false); + await _proxyCore.StopAsync().ConfigureAwait(false); - StatusText = "Starting VPN…"; - var tunFd = await AppServices.Platform.EstablishVpnAsync().ConfigureAwait(true); + RunOnUiThread(() => StatusText = "Starting VPN…"); + var tunFd = await AppServices.Platform.EstablishVpnAsync().ConfigureAwait(false); if (tunFd is null) { - StatusText = GetAndroidVpnFailureMessage(); + RunOnUiThread(() => StatusText = GetAndroidVpnFailureMessage()); return; } markVpnEngaged(true); - StatusText = $"Starting proxy for {server.Name}…"; - await _proxyCore.StartAsync(server, settings, tunFd).ConfigureAwait(true); - await AppServices.Platform.EnableProxyAsync().ConfigureAwait(true); - StatusText = $"Connected — {server.Name} (VPN)"; - IsConnected = true; + RunOnUiThread(() => StatusText = $"Starting proxy for {server.Name}…"); + await _proxyCore.StartAsync(server, settings, tunFd).ConfigureAwait(false); + await AppServices.Platform.EnableProxyAsync().ConfigureAwait(false); + + RunOnUiThread(() => + { + StatusText = $"Connected — {server.Name} (VPN)"; + IsConnected = true; + }); } private async Task SafeTeardownAsync(bool vpnEngaged) @@ -501,7 +513,7 @@ private async Task SafeTeardownAsync(bool vpnEngaged) } } - IsConnected = false; + RunOnUiThread(() => IsConnected = false); } private Task EmergencyDisconnectAsync() => SafeTeardownAsync(vpnEngaged: true);