Init
This commit is contained in:
85
src/config.rs
Normal file
85
src/config.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerSettings {
|
||||
pub local_port: u16,
|
||||
pub proxy_type: String, // "SOCKS" or "HTTP"
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub subscription_url: String,
|
||||
pub xray_binary_path: String,
|
||||
#[serde(default)]
|
||||
pub server_settings: HashMap<String, ServerSettings>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
subscription_url: String::new(),
|
||||
xray_binary_path: String::new(),
|
||||
server_settings: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Get the config file path in AppData
|
||||
pub fn get_config_path() -> Result<PathBuf, String> {
|
||||
// Get AppData\Roaming path
|
||||
let appdata = std::env::var("APPDATA")
|
||||
.map_err(|_| "Failed to get APPDATA environment variable".to_string())?;
|
||||
|
||||
let mut config_dir = PathBuf::from(appdata);
|
||||
config_dir.push("win-test-tray");
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir)
|
||||
.map_err(|e| format!("Failed to create config directory: {}", e))?;
|
||||
}
|
||||
|
||||
config_dir.push("config.json");
|
||||
Ok(config_dir)
|
||||
}
|
||||
|
||||
/// Load config from AppData
|
||||
pub fn load() -> Result<Config, String> {
|
||||
let config_path = Self::get_config_path()?;
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(Config::default());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
||||
|
||||
let config: Config = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse config JSON: {}", e))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Save config to AppData
|
||||
pub fn save(&self) -> Result<(), String> {
|
||||
let config_path = Self::get_config_path()?;
|
||||
|
||||
let json = serde_json::to_string_pretty(self)
|
||||
.map_err(|e| format!("Failed to serialize config: {}", e))?;
|
||||
|
||||
fs::write(&config_path, json)
|
||||
.map_err(|e| format!("Failed to write config file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
179
src/main.rs
Normal file
179
src/main.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
#![windows_subsystem = "windows"] // Commented out for debugging
|
||||
|
||||
mod ui;
|
||||
mod vpn;
|
||||
mod config;
|
||||
mod xray_manager;
|
||||
|
||||
use tray_icon::menu::{MenuEvent, MenuItem};
|
||||
use tray_icon::TrayIcon;
|
||||
use std::sync::{Arc, Mutex, LazyLock};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::HWND,
|
||||
UI::WindowsAndMessaging::*,
|
||||
},
|
||||
};
|
||||
|
||||
// Global tokio runtime for async operations
|
||||
pub static TOKIO_RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
|
||||
tokio::runtime::Runtime::new().expect("Failed to create tokio runtime")
|
||||
});
|
||||
|
||||
// Flag to trigger menu update
|
||||
pub static MENU_UPDATE_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Request menu update (can be called from any thread)
|
||||
pub fn request_menu_update() {
|
||||
MENU_UPDATE_REQUESTED.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Restart all xray servers based on current config
|
||||
/// This stops all running servers and starts enabled ones
|
||||
pub fn restart_xray_servers() {
|
||||
// Stop all running servers first
|
||||
TOKIO_RUNTIME.block_on(async {
|
||||
let _ = xray_manager::stop_all_servers().await;
|
||||
});
|
||||
|
||||
// Load config and start enabled servers
|
||||
if let Ok(config) = config::Config::load() {
|
||||
if !config.subscription_url.is_empty() && !config.xray_binary_path.is_empty() {
|
||||
// Fetch subscription URIs synchronously
|
||||
let subscription_uris = vpn::fetch_subscription_uris(&config.subscription_url);
|
||||
let mut servers = vpn::fetch_and_process_vpn_list(&config.subscription_url);
|
||||
vpn::assign_local_ports(&mut servers, &config.server_settings);
|
||||
|
||||
// Update global VPN_SERVERS state
|
||||
if let Ok(mut global_servers) = vpn::VPN_SERVERS.lock() {
|
||||
*global_servers = Some(servers.clone());
|
||||
}
|
||||
|
||||
// Start enabled servers
|
||||
TOKIO_RUNTIME.block_on(async {
|
||||
for server in servers.iter() {
|
||||
if server.enabled {
|
||||
let server_key = server.get_server_key();
|
||||
|
||||
if let Some(settings) = config.server_settings.get(&server_key) {
|
||||
if let Some(uri) = subscription_uris.get(&server_key) {
|
||||
match xray_manager::start_server(
|
||||
&server_key,
|
||||
uri,
|
||||
settings.local_port,
|
||||
&settings.proxy_type,
|
||||
&config.xray_binary_path,
|
||||
).await {
|
||||
Ok(_) => println!("Started server: {}", server.name),
|
||||
Err(e) => eprintln!("Failed to start server {}: {}", server.name, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Request menu update
|
||||
request_menu_update();
|
||||
}
|
||||
|
||||
/// Update tray icon menu with current running servers
|
||||
pub fn update_tray_menu(tray_icon: &mut TrayIcon, settings_item: &MenuItem, quit_item: &MenuItem) {
|
||||
let new_menu = ui::create_tray_menu_with_servers(settings_item, quit_item);
|
||||
tray_icon.set_menu(Some(Box::new(new_menu)));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Enable DPI awareness at process start
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||
let _ = SetProcessDpiAwarenessContext(
|
||||
windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-start servers on first launch
|
||||
restart_xray_servers();
|
||||
|
||||
// Create menu items
|
||||
let settings_item = MenuItem::new("Settings", true, None);
|
||||
let quit_item = MenuItem::new("Exit", true, None);
|
||||
|
||||
// Create tray icon with running servers list
|
||||
let mut tray_icon = ui::create_tray_icon_with_servers(&settings_item, &quit_item);
|
||||
|
||||
// Event handling
|
||||
let menu_channel = MenuEvent::receiver();
|
||||
|
||||
// Shared state for settings window
|
||||
#[cfg(windows)]
|
||||
let settings_window: Arc<Mutex<Option<HWND>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
// Windows message loop
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let settings_window_clone = settings_window.clone();
|
||||
|
||||
unsafe {
|
||||
let mut msg = MSG::default();
|
||||
|
||||
// Process Windows messages
|
||||
loop {
|
||||
// Check if menu update requested
|
||||
if MENU_UPDATE_REQUESTED.load(Ordering::Relaxed) {
|
||||
update_tray_menu(&mut tray_icon, &settings_item, &quit_item);
|
||||
MENU_UPDATE_REQUESTED.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// Check for menu events first
|
||||
if let Ok(event) = menu_channel.try_recv() {
|
||||
if event.id == settings_item.id() {
|
||||
// Open or focus settings window
|
||||
let mut window = settings_window_clone.lock().unwrap();
|
||||
if let Some(hwnd) = *window {
|
||||
// Window already exists, bring it to front
|
||||
if IsWindow(hwnd).as_bool() {
|
||||
let _ = ShowWindow(hwnd, SW_RESTORE);
|
||||
let _ = SetForegroundWindow(hwnd);
|
||||
} else {
|
||||
// Window was closed, create new one
|
||||
*window = Some(ui::create_settings_window());
|
||||
}
|
||||
} else {
|
||||
// Create new settings window
|
||||
*window = Some(ui::create_settings_window());
|
||||
}
|
||||
} else if event.id == quit_item.id() {
|
||||
// Stop all xray processes before exit
|
||||
TOKIO_RUNTIME.block_on(async {
|
||||
let _ = xray_manager::stop_all_servers().await;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to update tray menu (poll for changes)
|
||||
// This is not ideal but tray-icon doesn't support callbacks
|
||||
// In a real app, you'd use a channel or event system
|
||||
|
||||
// Process Windows messages
|
||||
let result = GetMessageW(&mut msg, None, 0, 0);
|
||||
if result.0 == 0 {
|
||||
// WM_QUIT received
|
||||
break;
|
||||
}
|
||||
|
||||
if result.0 > 0 {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/ui/mod.rs
Normal file
5
src/ui/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod tray;
|
||||
pub mod settings_window;
|
||||
|
||||
pub use tray::{create_tray_icon_with_servers, create_tray_menu_with_servers};
|
||||
pub use settings_window::create_settings_window;
|
||||
1227
src/ui/settings_window.rs
Normal file
1227
src/ui/settings_window.rs
Normal file
File diff suppressed because it is too large
Load Diff
143
src/ui/tray.rs
Normal file
143
src/ui/tray.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use tray_icon::{
|
||||
menu::{Menu, MenuItem, PredefinedMenuItem},
|
||||
TrayIconBuilder,
|
||||
};
|
||||
|
||||
pub fn create_tray_menu_with_servers(
|
||||
settings_item: &MenuItem,
|
||||
quit_item: &MenuItem,
|
||||
) -> Menu {
|
||||
// Create tray menu
|
||||
let tray_menu = Menu::new();
|
||||
|
||||
// Add running servers section
|
||||
let running_servers = crate::xray_manager::get_running_servers();
|
||||
if !running_servers.is_empty() {
|
||||
// Get server names from global VPN_SERVERS
|
||||
if let Ok(global_servers) = crate::vpn::VPN_SERVERS.lock() {
|
||||
if let Some(servers) = global_servers.as_ref() {
|
||||
for server in servers {
|
||||
let server_key = server.get_server_key();
|
||||
if running_servers.contains(&server_key) {
|
||||
let status_text = format!("✓ {} ({}:{})", server.name, server.proxy_type, server.local_port);
|
||||
let server_item = MenuItem::new(status_text, false, None);
|
||||
tray_menu.append(&server_item).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tray_menu.append(&PredefinedMenuItem::separator()).unwrap();
|
||||
}
|
||||
|
||||
// Append settings and quit items
|
||||
tray_menu.append_items(&[
|
||||
settings_item,
|
||||
&PredefinedMenuItem::separator(),
|
||||
quit_item,
|
||||
]).unwrap();
|
||||
|
||||
tray_menu
|
||||
}
|
||||
|
||||
pub fn create_tray_icon_with_servers(
|
||||
settings_item: &MenuItem,
|
||||
quit_item: &MenuItem,
|
||||
) -> tray_icon::TrayIcon {
|
||||
// Create menu
|
||||
let tray_menu = create_tray_menu_with_servers(settings_item, quit_item);
|
||||
|
||||
// Create icon (32x32 red square)
|
||||
let icon = create_icon();
|
||||
|
||||
// Create tray icon with context menu
|
||||
TrayIconBuilder::new()
|
||||
.with_menu(Box::new(tray_menu))
|
||||
.with_tooltip("VPN Manager")
|
||||
.with_icon(icon)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn create_tray_icon(
|
||||
settings_item: &MenuItem,
|
||||
quit_item: &MenuItem,
|
||||
) -> tray_icon::TrayIcon {
|
||||
// Create tray menu
|
||||
let tray_menu = Menu::new();
|
||||
|
||||
// Append items to menu
|
||||
tray_menu.append_items(&[
|
||||
settings_item,
|
||||
&PredefinedMenuItem::separator(),
|
||||
quit_item,
|
||||
]).unwrap();
|
||||
|
||||
// Create icon (32x32 red square)
|
||||
let icon = create_icon();
|
||||
|
||||
// Create tray icon with context menu
|
||||
TrayIconBuilder::new()
|
||||
.with_menu(Box::new(tray_menu))
|
||||
.with_tooltip("VPN Manager")
|
||||
.with_icon(icon)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn create_icon() -> tray_icon::Icon {
|
||||
// Create yellow star icon 32x32
|
||||
let width = 32;
|
||||
let height = 32;
|
||||
let mut rgba = Vec::with_capacity((width * height * 4) as usize);
|
||||
|
||||
// Define colors
|
||||
let bg = [0, 0, 0, 0]; // Transparent background
|
||||
let star = [255, 215, 0, 255]; // Gold/Yellow
|
||||
let border = [218, 165, 32, 255]; // Darker gold
|
||||
|
||||
let cx = 16.0;
|
||||
let cy = 16.0;
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let px = x as f32;
|
||||
let py = y as f32;
|
||||
|
||||
// Calculate angle and distance from center
|
||||
let dx = px - cx;
|
||||
let dy = py - cy;
|
||||
let angle = dy.atan2(dx);
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
|
||||
// 5-pointed star calculation
|
||||
// Star has 5 points, so we check angle modulo (2π/5)
|
||||
let point_angle = (angle + std::f32::consts::PI * 2.5) % (std::f32::consts::PI * 2.0 / 5.0);
|
||||
let normalized = point_angle / (std::f32::consts::PI * 2.0 / 5.0);
|
||||
|
||||
// Star shape: outer radius at points, inner radius between points
|
||||
let outer_radius = 12.0;
|
||||
let inner_radius = 5.0;
|
||||
|
||||
// Calculate radius at this angle (sine wave between inner and outer)
|
||||
let target_radius = if normalized < 0.5 {
|
||||
inner_radius + (outer_radius - inner_radius) * (normalized * 2.0)
|
||||
} else {
|
||||
outer_radius - (outer_radius - inner_radius) * ((normalized - 0.5) * 2.0)
|
||||
};
|
||||
|
||||
// Check if point is inside star
|
||||
let is_star = dist <= target_radius;
|
||||
let is_border = dist <= target_radius + 0.8 && dist > target_radius - 0.5;
|
||||
|
||||
if is_star && !is_border {
|
||||
rgba.extend_from_slice(&star);
|
||||
} else if is_border {
|
||||
rgba.extend_from_slice(&border);
|
||||
} else {
|
||||
rgba.extend_from_slice(&bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tray_icon::Icon::from_rgba(rgba, width, height).expect("Failed to create icon")
|
||||
}
|
||||
169
src/vpn/mod.rs
Normal file
169
src/vpn/mod.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use std::sync::Mutex;
|
||||
use std::collections::HashSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Global state for VPN servers
|
||||
pub static VPN_SERVERS: Mutex<Option<Vec<VpnServer>>> = Mutex::new(None);
|
||||
|
||||
// VPN server information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VpnServer {
|
||||
pub protocol: String,
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
pub local_port: u16, // User-defined local port
|
||||
pub proxy_type: String, // "HTTP" or "SOCKS"
|
||||
}
|
||||
|
||||
impl VpnServer {
|
||||
/// Get unique server key for stable identification
|
||||
pub fn get_server_key(&self) -> String {
|
||||
format!("{}://{}:{}", self.protocol, self.address, self.port)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign local ports to servers, preserving saved settings from config
|
||||
pub fn assign_local_ports(servers: &mut [VpnServer], saved_settings: &std::collections::HashMap<String, crate::config::ServerSettings>) {
|
||||
let mut used_ports = HashSet::new();
|
||||
|
||||
// First pass: assign saved settings (port + proxy type + enabled)
|
||||
for server in servers.iter_mut() {
|
||||
let key = server.get_server_key();
|
||||
if let Some(settings) = saved_settings.get(&key) {
|
||||
server.local_port = settings.local_port;
|
||||
server.proxy_type = settings.proxy_type.clone();
|
||||
server.enabled = settings.enabled;
|
||||
used_ports.insert(settings.local_port);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: assign new ports to servers without saved settings
|
||||
let mut next_port = 1080;
|
||||
for server in servers.iter_mut() {
|
||||
if server.local_port == 0 { // Not assigned yet
|
||||
while used_ports.contains(&next_port) {
|
||||
next_port += 1;
|
||||
}
|
||||
server.local_port = next_port;
|
||||
used_ports.insert(next_port);
|
||||
next_port += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and process VPN subscription list
|
||||
pub fn fetch_and_process_vpn_list(url: &str) -> Vec<VpnServer> {
|
||||
let mut servers = Vec::new();
|
||||
|
||||
// Fetch content from URL
|
||||
match reqwest::blocking::get(url) {
|
||||
Ok(response) => {
|
||||
match response.text() {
|
||||
Ok(content) => {
|
||||
// Decode from base64
|
||||
match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, content.trim()) {
|
||||
Ok(decoded_bytes) => {
|
||||
match String::from_utf8(decoded_bytes) {
|
||||
Ok(decoded_text) => {
|
||||
// Parse VPN links
|
||||
for line in decoded_text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(server) = parse_vpn_uri(trimmed) {
|
||||
servers.push(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
servers
|
||||
}
|
||||
|
||||
// Fetch subscription and return HashMap of server_key -> original_uri
|
||||
pub fn fetch_subscription_uris(url: &str) -> std::collections::HashMap<String, String> {
|
||||
let mut uris = std::collections::HashMap::new();
|
||||
|
||||
// Fetch content from URL
|
||||
match reqwest::blocking::get(url) {
|
||||
Ok(response) => {
|
||||
match response.text() {
|
||||
Ok(content) => {
|
||||
// Decode from base64
|
||||
match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, content.trim()) {
|
||||
Ok(decoded_bytes) => {
|
||||
match String::from_utf8(decoded_bytes) {
|
||||
Ok(decoded_text) => {
|
||||
// Parse VPN links
|
||||
for line in decoded_text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(server) = parse_vpn_uri(trimmed) {
|
||||
let key = server.get_server_key();
|
||||
uris.insert(key, trimmed.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
uris
|
||||
}
|
||||
|
||||
// Parse VPN URI using v2parser (supports vless, vmess, trojan, shadowsocks, socks)
|
||||
fn parse_vpn_uri(uri: &str) -> Option<VpnServer> {
|
||||
// Check if it's a supported protocol
|
||||
let is_supported = uri.starts_with("vless://")
|
||||
|| uri.starts_with("vmess://")
|
||||
|| uri.starts_with("trojan://")
|
||||
|| uri.starts_with("ss://")
|
||||
|| uri.starts_with("shadowsocks://")
|
||||
|| uri.starts_with("socks://");
|
||||
|
||||
if !is_supported {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Use v2parser to get metadata
|
||||
match std::panic::catch_unwind(|| v2parser::parser::get_metadata(uri)) {
|
||||
Ok(metadata_json) => {
|
||||
if let Ok(metadata) = serde_json::from_str::<serde_json::Value>(&metadata_json) {
|
||||
let protocol = metadata["protocol"].as_str()?.to_uppercase();
|
||||
let address = metadata["address"].as_str()?.to_string();
|
||||
let port = metadata["port"].as_u64()? as u16;
|
||||
let name = metadata["name"].as_str().unwrap_or("Unnamed").to_string();
|
||||
|
||||
Some(VpnServer {
|
||||
protocol,
|
||||
address,
|
||||
port,
|
||||
name,
|
||||
enabled: false, // Default to disabled, will be enabled from config
|
||||
local_port: 0, // Will be assigned by assign_local_ports
|
||||
proxy_type: "SOCKS".to_string(), // Default to SOCKS
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
72
src/xray_manager.rs
Normal file
72
src/xray_manager.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, LazyLock};
|
||||
use v2parser::xray_runner::XrayRunner;
|
||||
use v2parser::parser;
|
||||
|
||||
// Global state for running xray processes
|
||||
pub static XRAY_PROCESSES: LazyLock<Mutex<HashMap<String, XrayRunner>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// Start xray server for a specific VPN server
|
||||
/// Returns Ok if successful
|
||||
pub async fn start_server(
|
||||
server_key: &str,
|
||||
uri: &str,
|
||||
local_port: u16,
|
||||
proxy_type: &str,
|
||||
xray_binary_path: &str,
|
||||
) -> Result<(), String> {
|
||||
// Determine ports based on proxy type
|
||||
let (socks_port, http_port) = match proxy_type {
|
||||
"SOCKS" => (Some(local_port), None),
|
||||
"HTTP" => (None, Some(local_port)),
|
||||
_ => (Some(local_port), None), // Default to SOCKS
|
||||
};
|
||||
|
||||
// Generate xray config from URI
|
||||
let config_json = parser::create_json_config(uri, socks_port, http_port);
|
||||
|
||||
// Create and start xray runner
|
||||
let mut runner = XrayRunner::new();
|
||||
runner.start(&config_json, xray_binary_path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to start xray: {}", e))?;
|
||||
|
||||
// Store runner in global state
|
||||
if let Ok(mut processes) = XRAY_PROCESSES.lock() {
|
||||
processes.insert(server_key.to_string(), runner);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop xray server for a specific server
|
||||
pub async fn stop_server(server_key: &str) -> Result<(), String> {
|
||||
if let Ok(mut processes) = XRAY_PROCESSES.lock() {
|
||||
if let Some(mut runner) = processes.remove(server_key) {
|
||||
runner.stop()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to stop xray: {}", e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop all running xray servers
|
||||
pub async fn stop_all_servers() -> Result<(), String> {
|
||||
if let Ok(mut processes) = XRAY_PROCESSES.lock() {
|
||||
for (_key, mut runner) in processes.drain() {
|
||||
let _ = runner.stop().await; // Ignore errors during bulk shutdown
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get list of running server keys
|
||||
pub fn get_running_servers() -> Vec<String> {
|
||||
if let Ok(processes) = XRAY_PROCESSES.lock() {
|
||||
processes.keys().cloned().collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user