Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions docs/releases/v1.1.7.md
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions src/v2rayF.Android/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
2 changes: 2 additions & 0 deletions src/v2rayF.Android/Services/AndroidCoreEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
142 changes: 142 additions & 0 deletions src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Starts Xray via Android ProcessBuilder — System.Diagnostics.Process crashes on many devices.
/// </summary>
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;

Check warning on line 58 in src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs

View workflow job for this annotation

GitHub Actions / build-android

Converting null literal or possible null value to non-nullable type.
}

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.
}
}
}
2 changes: 2 additions & 0 deletions src/v2rayF.Core/Services/AppServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public static class AppServices

public static IPlatformIntegration Platform { get; set; } = null!;

public static ICoreProcessHost CoreProcessHost { get; set; } = new ManagedCoreProcessHost();

/// <summary>Called when the Android activity stops — tear down VPN so network is not left hijacked.</summary>
public static Func<Task>? EmergencyDisconnectAsync { get; set; }
}
2 changes: 2 additions & 0 deletions src/v2rayF.Core/Services/ICoreEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface ICoreEnvironment
string GetCoresDirectory();

string GetDataDirectory();

ICoreProcessHost CreateProcessHost();
}
21 changes: 21 additions & 0 deletions src/v2rayF.Core/Services/ICoreProcessHost.cs
Original file line number Diff line number Diff line change
@@ -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();
}
77 changes: 15 additions & 62 deletions src/v2rayF.Core/Services/LatencyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ public sealed class LatencyService
];

private readonly ICoreEnvironment _environment;
private readonly ICoreProcessHost _speedtestHost;
private readonly SemaphoreSlim _speedtestLock = new(1, 1);

public const int TimeoutMs = 10000;

public LatencyService(ICoreEnvironment environment)
{
_environment = environment;
_speedtestHost = environment.CreateProcessHost();
}

public async Task<int?> MeasureAsync(ProxyServer server, CancellationToken cancellationToken = default)
Expand All @@ -52,7 +54,6 @@ public LatencyService(ICoreEnvironment environment)
private async Task<int?> MeasureViaCoreAsync(ProxyServer server, CancellationToken cancellationToken)
{
await _speedtestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
Process? process = null;
try
{
var configDir = Path.Combine(_environment.GetDataDirectory(), "runtime");
Expand All @@ -63,52 +64,35 @@ 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)
.ConfigureAwait(false);
}
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)
Expand Down Expand Up @@ -137,37 +121,6 @@ private static async Task<bool> 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<int?> ProbeThroughSocksAsync(int socksPort, CancellationToken cancellationToken)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
Expand Down
Loading
Loading