base working button with saved state

This commit is contained in:
2026-03-18 19:59:07 +07:00
parent f3b51cf50c
commit 8144d64f58
33 changed files with 1085 additions and 193 deletions
+6 -2
View File
@@ -8,11 +8,15 @@ rust-version = "1.77.2"
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
links = "tauri-plugin-vpn"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
tauri = { version = "2.10.3" }
serde = "1.0"
tauri = { version = "2.0.0" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2"
netrunner-client = { git = "ssh://git@github.com/nineAp/netrunner-core.git", branch = "main" }
serde_json = "1.0.149"
[build-dependencies]
tauri-plugin = { version = "2.5.4", features = ["build"] }
@@ -36,5 +36,7 @@ dependencies {
implementation("net.java.dev.jna:jna:5.13.0@aar")
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation(project(":tauri-android"))
}
@@ -6,10 +6,13 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<service
android:name=".TunVpnService"
android:process=":vpn"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false"
android:foregroundServiceType="specialUse">
@@ -2,13 +2,16 @@ package app.netrunner.vpn
import android.app.Activity
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.os.Parcelable
import android.os.Build
import android.util.Log
import kotlinx.parcelize.Parcelize
import app.tauri.plugin.Plugin
import app.tauri.plugin.JSObject
import app.tauri.plugin.Invoke
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Command
@@ -23,6 +26,15 @@ import uniffi.netrunner_client.Session
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.webkit.WebView
import java.io.File
import java.net.URL
import java.net.HttpURLConnection
private const val TAG = "VPN_DEBUG"
@Parcelize
@@ -47,6 +59,16 @@ class TunVpnService : VpnService() {
Log.d(TAG, "TunVpnService created")
}
companion object {
var isRunning: Boolean = false
private set
// Метод для внешнего контроля (опционально)
fun setStatus(running: Boolean) {
isRunning = running
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand called action=${intent?.action}")
@@ -88,6 +110,17 @@ class TunVpnService : VpnService() {
return START_STICKY
}
private fun sendStatusBroadcast(status: String) {
val intent = Intent("com.netrunner.vpn.STATUS_UPDATE").apply {
putExtra("status", status)
// Указываем пакет нашего приложения, чтобы интент не улетел в другие аппки
// и гарантированно дошел до нашего VpnPlugin
setPackage(packageName)
}
sendBroadcast(intent)
Log.d("VPN_DEBUG", ">>> [SERVICE] Broadcast sent: $status")
}
private fun stopServiceProperly() {
stopVpn()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
@@ -98,9 +131,8 @@ class TunVpnService : VpnService() {
}
stopSelf()
}
private fun startVpn(config: TunnelConfig, ip: String, port: Int) {
Log.d(TAG, "startVpn called")
val builder = Builder()
@@ -109,16 +141,15 @@ class TunVpnService : VpnService() {
.addRoute("0.0.0.0", 0)
.setMtu(config.mtu)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
try {
builder.addDisallowedApplication("com.netrunner.vpn")
} catch (e: Exception) {
Log.d(TAG, "Can't exclude ${e.message}")
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
try {
// Исключаем само приложение из проксирования, чтобы не было петли
builder.addDisallowedApplication(packageName)
} catch (e: Exception) {
Log.e(TAG, "Can't exclude app: ${e.message}")
}
}
}
Log.d(TAG, "VPN builder configured")
vpnInterface = builder.establish()
if (vpnInterface == null) {
@@ -126,37 +157,40 @@ class TunVpnService : VpnService() {
return
}
Log.d(TAG, "VPN interface established")
val fd = vpnInterface?.detachFd() ?: run {
Log.e(TAG, "detachFd failed")
return
}
Log.d(TAG, "TUN fd=$fd")
try {
val manager = SessionManager()
val cachePath = cacheDir.absolutePath
Log.d(TAG, "Starting Rust Session with cachePath: $cachePath")
Log.d(TAG, "SessionManager created")
activeSession = manager.start("$ip:$port", fd)
Log.d(TAG, "Session started to $ip:$port")
activeSession = manager.startMobile("$ip:$port", fd, cachePath)
triggerVibration()
sendStatusBroadcast("connected")
isRunning = true
Log.d(TAG, "Session started successfully")
} catch (e: Exception) {
Log.e(TAG, "Rust manager.startMobile failed", e)
sendStatusBroadcast("idle")
stopVpn()
Log.e(TAG, "manager.start failed", e)
}
}
private fun stopVpn() {
Log.d(TAG, "stopVpn called")
triggerVibration()
sendStatusBroadcast("idle")
activeSession?.stop()
activeSession = null
isRunning = false
try {
vpnInterface?.close()
@@ -171,35 +205,66 @@ class TunVpnService : VpnService() {
private fun startForegroundNotification() {
val channelId = "vpn_service_channel"
val channelName = "VPN Service"
val channelName = "Netrunner VPN Service"
val notificationManager = getSystemService(android.app.NotificationManager::class.java)
// Создаем канал для Android 8.0+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val channel = android.app.NotificationChannel(
channelId, channelName, android.app.NotificationManager.IMPORTANCE_LOW
)
channelId, channelName, android.app.NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Показывает статус работы VPN"
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}
// Создаем Intent, чтобы при клике на уведомление открывалось приложение
val pendingIntent = android.app.PendingIntent.getActivity(
this, 0,
packageManager.getLaunchIntentForPackage(packageName),
android.app.PendingIntent.FLAG_IMMUTABLE
)
val notification = android.app.Notification.Builder(this, channelId)
.setContentTitle("Netrunner VPN")
.setContentText("VPN активен")
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) // Используй иконку из ресурсов
.setOngoing(true) // Пользователь не может смахнуть уведомление
.setContentText("Защита активна")
.setSmallIcon(android.R.drawable.ic_dialog_info) // Стандартная системная иконка
.setContentIntent(pendingIntent)
.setOngoing(true)
.setCategory(android.app.Notification.CATEGORY_SERVICE)
.build()
// Запуск Foreground с обязательным указанием типа для Android 14+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(1, notification)
try {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(1, notification)
}
Log.d(TAG, "Foreground notification started successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to start foreground: ${e.message}")
}
}
private fun triggerVibration() {
val vibrator = getSystemService(android.content.Context.VIBRATOR_SERVICE) as android.os.Vibrator
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
vibrator.vibrate(android.os.VibrationEffect.createOneShot(500, android.os.VibrationEffect.EFFECT_HEAVY_CLICK))
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(500)
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.d(TAG, "UI process killed (swiped), but VPN process should keep running.")
}
override fun onDestroy() {
isRunning = false
Log.d(TAG, "TunVpnService destroyed")
stopVpn()
sendStatusBroadcast("idle")
super.onDestroy()
}
}
@@ -211,12 +276,106 @@ class TunVpnService : VpnService() {
@TauriPlugin
class VpnPlugin(private val activity: Activity) : Plugin(activity) {
init {
Log.d(TAG, "VpnPlugin init")
System.loadLibrary("netrunner_client")
Log.d(TAG, "Rust library loaded")
companion object {
private var instance: VpnPlugin? = null
fun sendStatus(status: String) {
val hasInstance = (instance != null)
Log.d(TAG, ">>> [PLUGIN COMPANION] sendStatus called: $status | Instance exists: $hasInstance")
if (hasInstance) {
instance?.updateStatus(status)
} else {
Log.e(TAG, "CRITICAL: Cannot send status, VpnPlugin instance is NULL")
}
}
}
private val statusReceiver = object : android.content.BroadcastReceiver() {
override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
val status = intent?.getStringExtra("status") ?: return
Log.d(TAG, ">>> [RECEIVER] Got status from Service: $status")
instance?.updateStatus(status)
}
}
private fun registerStatusReceiver() {
val filter = IntentFilter("com.netrunner.vpn.STATUS_UPDATE")
if (Build.VERSION.SDK_INT >= 33) {
activity.registerReceiver(statusReceiver, filter, android.content.Context.RECEIVER_NOT_EXPORTED)
} else {
activity.registerReceiver(statusReceiver, filter)
}
Log.d(TAG, "Status receiver registered successfully")
}
override fun load(webView: WebView) {
super.load(webView)
instance = this
Log.d(TAG, "VpnPlugin loaded with WebView and instance assigned")
try {
System.loadLibrary("netrunner_client")
Log.d(TAG, "Rust library loaded")
} catch (e: Exception) {
Log.e(TAG, "Failed to load Rust library: ${e.message}")
}
requestNotificationPermission()
CoroutineScope(Dispatchers.IO).launch {
updateBlocklistIfNeeded()
}
registerStatusReceiver()
}
private fun updateBlocklistIfNeeded() {
try {
val cachePath = activity.cacheDir.absolutePath
val hostsFile = File(cachePath, "hosts_cache.txt")
val isFresh = hostsFile.exists() &&
(System.currentTimeMillis() - hostsFile.lastModified() < 24 * 60 * 60 * 1000)
if (isFresh) {
Log.d(TAG, "DNS: Blocklist is fresh, skipping download")
return
}
Log.d(TAG, "DNS: Starting blocklist download via Kotlin...")
// Качаем
val url = URL("https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts")
val connection = url.openConnection()
connection.connectTimeout = 15000
connection.readTimeout = 15000
val bytes = connection.getInputStream().use { it.readBytes() }
// Сохраняем
hostsFile.writeBytes(bytes)
Log.d(TAG, "DNS: Blocklist updated! Size: ${bytes.size} bytes")
} catch (e: Exception) {
Log.e(TAG, "DNS: Failed to download blocklist: ${e.message}")
}
}
private fun requestNotificationPermission() {
if (android.os.Build.VERSION.SDK_INT >= 33) {
val permission = "android.permission.POST_NOTIFICATIONS"
if (androidx.core.content.ContextCompat.checkSelfPermission(activity, permission) !=
android.content.pm.PackageManager.PERMISSION_GRANTED) {
androidx.core.app.ActivityCompat.requestPermissions(activity, arrayOf(permission), 101)
}
}
}
@InvokeArg
class VpnArgs {
var config: TunnelConfig? = null
@@ -285,6 +444,22 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
activity.startService(intent)
}
}
fun updateStatus(status: String) {
val data = JSObject()
data.put("status", status)
Log.d(TAG, ">>> [PLUGIN INSTANCE] trigger() called with event: status-change, data: $status")
try {
// Пробуем вызвать trigger. В Tauri 2.0 он должен прокинуть это в JS
trigger("status-change", data)
Log.d(TAG, ">>> [PLUGIN INSTANCE] trigger() executed successfully")
} catch (e: Exception) {
Log.e(TAG, "!!! [PLUGIN INSTANCE] trigger() FAILED: ${e.message}", e)
}
}
@ActivityCallback
fun onVpnPermissionResult(invoke: Invoke, result: ActivityResult) {
@@ -324,4 +499,14 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
invoke.resolve()
}
@Command
fun checkActualVpnStatus(invoke: Invoke) {
val status = if (TunVpnService.isRunning) "connected" else "idle"
val res = JSObject()
res.put("status", status)
invoke.resolve(res)
}
}
+7 -1
View File
@@ -1,6 +1,12 @@
// src-tauri/src/plugins/vpn-plugin/build.rs
const COMMANDS: &[&str] = &["start_tunnel", "stop_tunnel"];
const COMMANDS: &[&str] = &[
"start_vpn",
"stop_vpn",
"check_actual_vpn_status",
"registerListener",
"unregisterListener",
];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
@@ -1,5 +1,4 @@
import { invoke } from "@tauri-apps/api/core";
import { addPluginListener, invoke } from "@tauri-apps/api/core";
export interface TunnelConfig {
address: string;
prefix: number;
@@ -18,3 +17,24 @@ export async function startVpn(config: TunnelConfig, ip: string, port: number) {
export async function stopVpn() {
return await invoke("plugin:vpn|stop_vpn");
}
export async function checkActualVpnStatus(): Promise<"connected" | "idle"> {
try {
console.log("Actual vpn status called");
const result = await invoke<{ status: string }>(
"plugin:vpn|check_actual_vpn_status",
);
console.log(result);
return result.status as "connected" | "idle";
} catch (e) {
console.error("Failed to check VPN status", e);
return "idle";
}
}
export const setupVpnStatusListener = async (callback: (p: any) => void) => {
return await addPluginListener("vpn", "status-change", (payload) => {
console.log(">>> [SUCCESS] Пришло через мобильный листенер:", payload);
callback(payload);
});
};
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-check-actual-vpn-status"
description = "Enables the check_actual_vpn_status command without any pre-configured scope."
commands.allow = ["check_actual_vpn_status"]
[[permission]]
identifier = "deny-check-actual-vpn-status"
description = "Denies the check_actual_vpn_status command without any pre-configured scope."
commands.deny = ["check_actual_vpn_status"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-registerListener"
description = "Enables the registerListener command without any pre-configured scope."
commands.allow = ["registerListener"]
[[permission]]
identifier = "deny-registerListener"
description = "Denies the registerListener command without any pre-configured scope."
commands.deny = ["registerListener"]
@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-start-tunnel"
description = "Enables the start_tunnel command without any pre-configured scope."
commands.allow = ["start_tunnel"]
[[permission]]
identifier = "deny-start-tunnel"
description = "Denies the start_tunnel command without any pre-configured scope."
commands.deny = ["start_tunnel"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-start-vpn"
description = "Enables the start_vpn command without any pre-configured scope."
commands.allow = ["start_vpn"]
[[permission]]
identifier = "deny-start-vpn"
description = "Denies the start_vpn command without any pre-configured scope."
commands.deny = ["start_vpn"]
@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-stop-tunnel"
description = "Enables the stop_tunnel command without any pre-configured scope."
commands.allow = ["stop_tunnel"]
[[permission]]
identifier = "deny-stop-tunnel"
description = "Denies the stop_tunnel command without any pre-configured scope."
commands.deny = ["stop_tunnel"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-stop-vpn"
description = "Enables the stop_vpn command without any pre-configured scope."
commands.allow = ["stop_vpn"]
[[permission]]
identifier = "deny-stop-vpn"
description = "Denies the stop_vpn command without any pre-configured scope."
commands.deny = ["stop_vpn"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-unregisterListener"
description = "Enables the unregisterListener command without any pre-configured scope."
commands.allow = ["unregisterListener"]
[[permission]]
identifier = "deny-unregisterListener"
description = "Denies the unregisterListener command without any pre-configured scope."
commands.deny = ["unregisterListener"]
@@ -10,12 +10,12 @@
<tr>
<td>
`vpn:allow-start-tunnel`
`vpn:allow-check-actual-vpn-status`
</td>
<td>
Enables the start_tunnel command without any pre-configured scope.
Enables the check_actual_vpn_status command without any pre-configured scope.
</td>
</tr>
@@ -23,12 +23,12 @@ Enables the start_tunnel command without any pre-configured scope.
<tr>
<td>
`vpn:deny-start-tunnel`
`vpn:deny-check-actual-vpn-status`
</td>
<td>
Denies the start_tunnel command without any pre-configured scope.
Denies the check_actual_vpn_status command without any pre-configured scope.
</td>
</tr>
@@ -36,12 +36,12 @@ Denies the start_tunnel command without any pre-configured scope.
<tr>
<td>
`vpn:allow-stop-tunnel`
`vpn:allow-registerListener`
</td>
<td>
Enables the stop_tunnel command without any pre-configured scope.
Enables the registerListener command without any pre-configured scope.
</td>
</tr>
@@ -49,12 +49,90 @@ Enables the stop_tunnel command without any pre-configured scope.
<tr>
<td>
`vpn:deny-stop-tunnel`
`vpn:deny-registerListener`
</td>
<td>
Denies the stop_tunnel command without any pre-configured scope.
Denies the registerListener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:allow-start-vpn`
</td>
<td>
Enables the start_vpn command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:deny-start-vpn`
</td>
<td>
Denies the start_vpn command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:allow-stop-vpn`
</td>
<td>
Enables the stop_vpn command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:deny-stop-vpn`
</td>
<td>
Denies the stop_vpn command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:allow-unregisterListener`
</td>
<td>
Enables the unregisterListener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`vpn:deny-unregisterListener`
</td>
<td>
Denies the unregisterListener command without any pre-configured scope.
</td>
</tr>
@@ -3,4 +3,7 @@ identifier = "allow-vpn-control"
description = "Allows Vpn Control"
[permission.commands]
allow = ["start_vpn", "stop_vpn"]
allow = ["start_vpn", "stop_vpn", "check_actual_vpn_status", "registerListener", "unregisterListener"]
[permission.events]
allow = ["status-change"]
@@ -295,28 +295,64 @@
"type": "string",
"oneOf": [
{
"description": "Enables the start_tunnel command without any pre-configured scope.",
"description": "Enables the check_actual_vpn_status command without any pre-configured scope.",
"type": "string",
"const": "allow-start-tunnel",
"markdownDescription": "Enables the start_tunnel command without any pre-configured scope."
"const": "allow-check-actual-vpn-status",
"markdownDescription": "Enables the check_actual_vpn_status command without any pre-configured scope."
},
{
"description": "Denies the start_tunnel command without any pre-configured scope.",
"description": "Denies the check_actual_vpn_status command without any pre-configured scope.",
"type": "string",
"const": "deny-start-tunnel",
"markdownDescription": "Denies the start_tunnel command without any pre-configured scope."
"const": "deny-check-actual-vpn-status",
"markdownDescription": "Denies the check_actual_vpn_status command without any pre-configured scope."
},
{
"description": "Enables the stop_tunnel command without any pre-configured scope.",
"description": "Enables the registerListener command without any pre-configured scope.",
"type": "string",
"const": "allow-stop-tunnel",
"markdownDescription": "Enables the stop_tunnel command without any pre-configured scope."
"const": "allow-registerListener",
"markdownDescription": "Enables the registerListener command without any pre-configured scope."
},
{
"description": "Denies the stop_tunnel command without any pre-configured scope.",
"description": "Denies the registerListener command without any pre-configured scope.",
"type": "string",
"const": "deny-stop-tunnel",
"markdownDescription": "Denies the stop_tunnel command without any pre-configured scope."
"const": "deny-registerListener",
"markdownDescription": "Denies the registerListener command without any pre-configured scope."
},
{
"description": "Enables the start_vpn command without any pre-configured scope.",
"type": "string",
"const": "allow-start-vpn",
"markdownDescription": "Enables the start_vpn command without any pre-configured scope."
},
{
"description": "Denies the start_vpn command without any pre-configured scope.",
"type": "string",
"const": "deny-start-vpn",
"markdownDescription": "Denies the start_vpn command without any pre-configured scope."
},
{
"description": "Enables the stop_vpn command without any pre-configured scope.",
"type": "string",
"const": "allow-stop-vpn",
"markdownDescription": "Enables the stop_vpn command without any pre-configured scope."
},
{
"description": "Denies the stop_vpn command without any pre-configured scope.",
"type": "string",
"const": "deny-stop-vpn",
"markdownDescription": "Denies the stop_vpn command without any pre-configured scope."
},
{
"description": "Enables the unregisterListener command without any pre-configured scope.",
"type": "string",
"const": "allow-unregisterListener",
"markdownDescription": "Enables the unregisterListener command without any pre-configured scope."
},
{
"description": "Denies the unregisterListener command without any pre-configured scope.",
"type": "string",
"const": "deny-unregisterListener",
"markdownDescription": "Denies the unregisterListener command without any pre-configured scope."
},
{
"description": "Allows Vpn Control",
@@ -47,3 +47,14 @@ pub(crate) fn stop_vpn<R: Runtime>(app: AppHandle<R>) -> crate::Result<()> {
eprintln!("--- [DEBUG] STOPPING VPN TUNNEL ---");
result
}
#[command]
pub(crate) fn check_actual_vpn_status<R: tauri::Runtime>(
app: tauri::AppHandle<R>,
) -> crate::Result<StatusResponse> {
// StatusResponse должен быть в твоих моделях
let vpn = app.state::<Vpn<R>>();
// Вызываем метод из vpn_impl (в mobile это уйдет в Kotlin)
vpn.check_actual_vpn_status()
}
@@ -41,4 +41,16 @@ impl<R: Runtime> VpnInterface<R> for Vpn<R> {
}
Ok(())
}
fn check_actual_vpn_status(&self) -> crate::Result<StatusResponse> {
let lock = self.active_session.lock().unwrap();
let status = if lock.is_some() {
"connected".to_string()
} else {
"idle".to_string()
};
Ok(StatusResponse { status })
}
}
+11 -4
View File
@@ -1,4 +1,8 @@
use std::{ops::Deref, sync::atomic::AtomicBool};
use std::{
ops::Deref,
sync::atomic::{AtomicBool, Ordering},
};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
@@ -16,11 +20,11 @@ mod error;
mod models;
pub use error::{Error, Result};
// В lib.rs
pub trait VpnInterface<R: Runtime>: Send + Sync + 'static {
fn start_vpn(&self, config: TunnelConfig, ip: String, port: i32) -> crate::Result<()>;
fn stop_vpn(&self) -> crate::Result<()>;
fn check_actual_vpn_status(&self) -> crate::Result<StatusResponse>;
}
pub struct Vpn<R: Runtime> {
inner: Box<dyn VpnInterface<R>>,
@@ -48,14 +52,17 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("vpn")
.invoke_handler(tauri::generate_handler![
commands::start_vpn,
commands::stop_vpn
commands::stop_vpn,
commands::check_actual_vpn_status
])
.setup(|app, api| {
#[cfg(mobile)]
let vpn_impl = mobile::init(app, api)?;
#[cfg(desktop)]
let vpn_impl = desktop::init(app, api)?;
app.manage(Vpn::new(vpn_impl));
let vpn_instance = Vpn::new(vpn_impl);
app.manage(vpn_instance);
Ok(())
})
.build()
+21 -1
View File
@@ -33,9 +33,29 @@ impl<R: Runtime> VpnInterface<R> for Vpn<R> {
fn stop_vpn(&self) -> crate::Result<()> {
self.send("stopVpn", StopVpnRequest {})
}
fn check_actual_vpn_status(&self) -> crate::Result<StatusResponse> {
println!(">>> [RUST PLUGIN] Calling Kotlin: checkActualVpnStatus");
self.0
.run_mobile_plugin::<StatusResponse>("checkActualVpnStatus", ())
.map(|res| {
println!(">>> [RUST PLUGIN] Success from Kotlin: {:?}", res);
res
})
.map_err(|e| {
println!(">>> [RUST PLUGIN] Critical Error calling Kotlin: {:?}", e);
let err_msg = format!("{:?}", e);
if err_msg.contains("CommandNotFound") {
println!(
"!!! [RUST PLUGIN] ДИАГНОЗ: Метод checkActualVpnStatus не найден в Kotlin."
);
}
crate::Error::from(e)
})
}
}
// Приватный метод для уменьшения дублирования кода
impl<R: Runtime> Vpn<R> {
fn send<S: serde::Serialize>(&self, cmd: &str, payload: S) -> crate::Result<()> {
self.0.run_mobile_plugin(cmd, payload).map_err(Into::into)
@@ -19,3 +19,9 @@ pub struct StartVpnRequest {
#[derive(Debug, Deserialize, Serialize)]
pub struct StopVpnRequest {}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StatusResponse {
pub status: String,
}