Added xray.exe downloading and autostart

This commit is contained in:
2025-12-27 00:25:33 +00:00
parent 9a550c39ab
commit ad2ea77a4f
5 changed files with 1365 additions and 29 deletions
+62
View File
@@ -21,6 +21,8 @@ pub struct Config {
pub xray_binary_path: String,
#[serde(default)]
pub server_settings: HashMap<String, ServerSettings>,
#[serde(default)]
pub autostart: bool,
}
impl Default for Config {
@@ -29,6 +31,7 @@ impl Default for Config {
subscription_url: String::new(),
xray_binary_path: String::new(),
server_settings: HashMap::new(),
autostart: false,
}
}
}
@@ -82,4 +85,63 @@ impl Config {
Ok(())
}
/// Set autostart in Windows registry
pub fn set_autostart(enabled: bool) -> Result<(), String> {
#[cfg(windows)]
{
use windows::{
core::w,
Win32::System::Registry::{
RegOpenKeyExW, RegSetValueExW, RegDeleteKeyValueW,
HKEY_CURRENT_USER, KEY_WRITE, REG_SZ,
},
};
let key_path = w!("Software\\Microsoft\\Windows\\CurrentVersion\\Run");
let value_name = w!("Xray-VPN-Manager");
unsafe {
let mut hkey = Default::default();
let result = RegOpenKeyExW(HKEY_CURRENT_USER, key_path, 0, KEY_WRITE, &mut hkey);
if result.is_err() {
return Err(format!("Failed to open registry key: {:?}", result));
}
if enabled {
// Get current executable path
let exe_path = std::env::current_exe()
.map_err(|e| format!("Failed to get exe path: {}", e))?;
let exe_path_str = exe_path.to_string_lossy().to_string();
let path_wide: Vec<u16> = format!("{}\0", exe_path_str).encode_utf16().collect();
let data = std::slice::from_raw_parts(
path_wide.as_ptr() as *const u8,
path_wide.len() * 2
);
let result = RegSetValueExW(
hkey,
value_name,
0,
REG_SZ,
Some(data),
);
if result.is_err() {
return Err(format!("Failed to set registry value: {:?}", result));
}
} else {
// Remove from autostart
let _ = RegDeleteKeyValueW(hkey, None, value_name);
}
}
}
#[cfg(not(windows))]
{
return Err("Autostart only supported on Windows".to_string());
}
Ok(())
}
}
+332 -12
View File
@@ -3,7 +3,7 @@ use windows::{
core::{PCWSTR, w},
Win32::{
Foundation::{HWND, LPARAM, LRESULT, WPARAM, HINSTANCE, RECT, BOOL},
Graphics::Gdi::{UpdateWindow, HBRUSH, SetBkMode, TRANSPARENT, HDC, GetStockObject, WHITE_BRUSH},
Graphics::Gdi::{UpdateWindow, HBRUSH, SetBkMode, TRANSPARENT, HDC, GetStockObject, WHITE_BRUSH, InvalidateRect},
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::*,
},
@@ -23,11 +23,16 @@ const ID_SAVE_BUTTON: i32 = 1005;
const ID_CANCEL_BUTTON: i32 = 1006;
const ID_XRAY_PATH_EDIT: i32 = 1007;
const ID_XRAY_BROWSE_BUTTON: i32 = 1008;
const ID_XRAY_DOWNLOAD_BUTTON: i32 = 1009;
const ID_AUTOSTART_CHECKBOX: i32 = 1010;
const ID_SERVER_CHECKBOX_BASE: i32 = 2000; // 2000, 2001, 2002...
const ID_SERVER_PORT_EDIT_BASE: i32 = 3000; // 3000, 3001, 3002...
const ID_SERVER_PROXY_COMBO_BASE: i32 = 4000; // 4000, 4001, 4002...
const ID_SERVER_LABEL_BASE: i32 = 5000; // 5000, 5001, 5002... for "Proxy Port:" labels
// Custom Windows message for download completion
const WM_DOWNLOAD_COMPLETE: u32 = WM_USER + 2;
// Layout constants for consistent formatting
const MARGIN: i32 = 15;
const FONT_SIZE: i32 = 32; // Reduced from 40
@@ -341,7 +346,32 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
unsafe { SendMessageW(lbl, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); }
}
// Xray binary path edit control
// Download button (above the path field) - same width as URL edit control
let download_btn_text: Vec<u16> = "Download Xray Automatically\0".encode_utf16().collect();
let download_btn = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
w!("BUTTON"),
PCWSTR::from_raw(download_btn_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_PUSHBUTTON as u32),
MARGIN + URL_LABEL_WIDTH + 10,
row2_y,
450,
CONTROL_HEIGHT,
parent,
HMENU(ID_XRAY_DOWNLOAD_BUTTON as _),
hinstance,
None,
).ok()
};
if let Some(btn) = download_btn {
unsafe { SendMessageW(btn, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); }
}
// Third row Y position (for path field)
let row3_y = row2_y + CONTROL_HEIGHT + MARGIN;
// Xray binary path edit control - same width as URL edit control
let xray_path_text_wide: Vec<u16> = format!("{}\0", config.xray_binary_path).encode_utf16().collect();
let xray_path_edit = unsafe {
CreateWindowExW(
@@ -350,7 +380,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
PCWSTR::from_raw(xray_path_text_wide.as_ptr()),
WS_CHILD | WS_VISIBLE | WS_BORDER | WINDOW_STYLE(ES_AUTOHSCROLL as u32),
MARGIN + URL_LABEL_WIDTH + 10,
row2_y,
row3_y,
450,
CONTROL_HEIGHT,
parent,
@@ -370,7 +400,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
PCWSTR::from_raw(browse_btn_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_PUSHBUTTON as u32),
MARGIN + URL_LABEL_WIDTH + 10 + 450 + 10,
row2_y,
row3_y,
120,
CONTROL_HEIGHT,
parent,
@@ -383,8 +413,42 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
unsafe { SendMessageW(btn, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1)); }
}
// Third row Y position
let row3_y = row2_y + CONTROL_HEIGHT + MARGIN;
// Fourth row Y position
let row4_y = row3_y + CONTROL_HEIGHT + MARGIN;
// Autostart checkbox
let autostart_text: Vec<u16> = "Start automatically on Windows startup\0".encode_utf16().collect();
let autostart_checkbox = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
w!("BUTTON"),
PCWSTR::from_raw(autostart_text.as_ptr()),
WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_AUTOCHECKBOX as u32),
MARGIN + URL_LABEL_WIDTH + 10,
row4_y,
450,
CONTROL_HEIGHT,
parent,
HMENU(ID_AUTOSTART_CHECKBOX as _),
hinstance,
None,
).ok()
};
if let Some(cb) = autostart_checkbox {
unsafe {
SendMessageW(cb, WM_SETFONT, WPARAM(hfont.0 as usize), LPARAM(1));
// Set checkbox state based on config
SendMessageW(
cb,
BM_SETCHECK,
WPARAM(if config.autostart { 1 } else { 0 }),
LPARAM(0),
);
}
}
// Fifth row Y position
let row5_y = row4_y + CONTROL_HEIGHT + MARGIN;
// Label "VPN Servers:"
let list_label: Vec<u16> = "VPN Servers:\0".encode_utf16().collect();
@@ -395,7 +459,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
PCWSTR::from_raw(list_label.as_ptr()),
WS_CHILD | WS_VISIBLE,
MARGIN,
row3_y,
row5_y,
200,
LABEL_HEIGHT,
parent,
@@ -409,7 +473,7 @@ unsafe fn create_controls(parent: HWND, hinstance: HINSTANCE) {
}
// Server list container Y position
let container_y = row3_y + LABEL_HEIGHT + 10;
let container_y = row5_y + LABEL_HEIGHT + 10;
// Get client area size to calculate container height dynamically
let mut client_rect = RECT::default();
@@ -694,6 +758,59 @@ unsafe extern "system" fn settings_window_proc(
}
}
}
// Handle Download button for Xray binary
else if control_id == ID_XRAY_DOWNLOAD_BUTTON as usize && notification_code == 0 {
println!("Download button clicked!");
// Disable download button during download
if let Ok(download_btn) = unsafe { GetDlgItem(hwnd, ID_XRAY_DOWNLOAD_BUTTON) } {
if !download_btn.is_invalid() {
unsafe {
// Disable by setting WS_DISABLED style
let style = GetWindowLongW(download_btn, GWL_STYLE);
SetWindowLongW(download_btn, GWL_STYLE, style | WS_DISABLED.0 as i32);
let _ = InvalidateRect(download_btn, None, true);
let downloading_text: Vec<u16> = "Downloading...\0".encode_utf16().collect();
SetWindowTextW(download_btn, PCWSTR::from_raw(downloading_text.as_ptr())).ok();
}
}
}
// Spawn background thread for download
let hwnd_raw = hwnd.0 as isize;
std::thread::spawn(move || {
match download_xray_latest() {
Ok(xray_exe_path) => {
println!("Downloaded Xray to: {}", xray_exe_path);
// Update UI on main thread
unsafe {
let hwnd = HWND(hwnd_raw as *mut _);
// Allocate string on heap for passing to main thread
let path_box = Box::new(xray_exe_path);
let path_ptr = Box::into_raw(path_box);
let _ = PostMessageW(hwnd, WM_DOWNLOAD_COMPLETE, WPARAM(1), LPARAM(path_ptr as isize));
}
}
Err(e) => {
eprintln!("Failed to download Xray: {}", e);
// Send error message
unsafe {
let hwnd = HWND(hwnd_raw as *mut _);
let error_box = Box::new(e);
let error_ptr = Box::into_raw(error_box);
let _ = PostMessageW(hwnd, WM_DOWNLOAD_COMPLETE, WPARAM(0), LPARAM(error_ptr as isize));
}
}
}
});
}
// Handle Save button
else if control_id == ID_SAVE_BUTTON as usize && notification_code == 0 {
@@ -725,6 +842,16 @@ unsafe extern "system" fn settings_window_proc(
String::new()
};
// Get autostart checkbox state
let autostart = unsafe {
if let Ok(checkbox) = GetDlgItem(hwnd, ID_AUTOSTART_CHECKBOX) {
let state = SendMessageW(checkbox, BM_GETCHECK, WPARAM(0), LPARAM(0));
state.0 == 1
} else {
false
}
};
// Build server_settings HashMap from current servers
use std::collections::HashMap;
let mut server_settings = HashMap::new();
@@ -748,10 +875,16 @@ unsafe extern "system" fn settings_window_proc(
subscription_url,
xray_binary_path,
server_settings,
autostart,
};
match config.save() {
Ok(_) => {
// Apply autostart setting to registry
if let Err(e) = crate::config::Config::set_autostart(autostart) {
eprintln!("Failed to set autostart: {}", e);
}
// Restart xray servers with new config
crate::restart_xray_servers();
@@ -799,7 +932,9 @@ unsafe extern "system" fn settings_window_proc(
let row1_y = MARGIN;
let row2_y = row1_y + CONTROL_HEIGHT + MARGIN;
let row3_y = row2_y + CONTROL_HEIGHT + MARGIN;
let container_y = row3_y + LABEL_HEIGHT + 10;
let row4_y = row3_y + CONTROL_HEIGHT + MARGIN;
let row5_y = row4_y + CONTROL_HEIGHT + MARGIN;
let container_y = row5_y + LABEL_HEIGHT + 10;
let container_height = height - container_y - MARGIN - BUTTON_ROW_HEIGHT;
let buttons_y = container_y + container_height + 10;
@@ -853,7 +988,7 @@ unsafe extern "system" fn settings_window_proc(
browse_btn,
None,
width - 120 - MARGIN,
row2_y,
row3_y,
0, 0,
SWP_NOSIZE | SWP_NOZORDER,
).ok();
@@ -918,6 +1053,65 @@ unsafe extern "system" fn settings_window_proc(
}
LRESULT(0)
}
_ if msg == WM_DOWNLOAD_COMPLETE => {
// Custom message: download complete
let success = wparam.0 == 1;
let data_ptr = lparam.0 as *mut String;
if success {
// Success - update path field
unsafe {
let path = Box::from_raw(data_ptr);
// Update edit control
if let Ok(xray_edit) = GetDlgItem(hwnd, ID_XRAY_PATH_EDIT) {
let path_wide: Vec<u16> = format!("{}\0", path).encode_utf16().collect();
SetWindowTextW(xray_edit, PCWSTR::from_raw(path_wide.as_ptr())).ok();
}
// Show success message
let msg: Vec<u16> = "Xray binary downloaded successfully!\0".encode_utf16().collect();
let title: Vec<u16> = "Success\0".encode_utf16().collect();
MessageBoxW(
hwnd,
PCWSTR::from_raw(msg.as_ptr()),
PCWSTR::from_raw(title.as_ptr()),
MB_OK | MB_ICONINFORMATION,
);
}
} else {
// Error - show error message
unsafe {
let error_msg = Box::from_raw(data_ptr);
let msg: Vec<u16> = format!("Failed to download Xray:\n{}\0", error_msg).encode_utf16().collect();
let title: Vec<u16> = "Error\0".encode_utf16().collect();
MessageBoxW(
hwnd,
PCWSTR::from_raw(msg.as_ptr()),
PCWSTR::from_raw(title.as_ptr()),
MB_OK | MB_ICONERROR,
);
}
}
// Re-enable download button
if let Ok(download_btn) = unsafe { GetDlgItem(hwnd, ID_XRAY_DOWNLOAD_BUTTON) } {
if !download_btn.is_invalid() {
unsafe {
// Re-enable by removing WS_DISABLED style
let style = GetWindowLongW(download_btn, GWL_STYLE);
SetWindowLongW(download_btn, GWL_STYLE, style & !(WS_DISABLED.0 as i32));
let _ = InvalidateRect(download_btn, None, true);
let download_text: Vec<u16> = "Download\0".encode_utf16().collect();
SetWindowTextW(download_btn, PCWSTR::from_raw(download_text.as_ptr())).ok();
}
}
}
LRESULT(0)
}
WM_DESTROY => {
println!("Settings window destroyed");
LRESULT(0)
@@ -1198,7 +1392,7 @@ unsafe fn resize_server_list_items(container: HWND) {
}
}
// Reposition combo
// Reposition combo (keep dropdown height at 200)
if let Ok(combo) = unsafe { GetDlgItem(container, ID_SERVER_PROXY_COMBO_BASE + idx as i32) } {
if !combo.is_invalid() {
unsafe {
@@ -1208,7 +1402,7 @@ unsafe fn resize_server_list_items(container: HWND) {
right_controls_x + LABEL_WIDTH + 5 + PORT_EDIT_WIDTH + 10,
y_pos,
COMBO_WIDTH,
CONTROL_HEIGHT,
200, // Keep dropdown height
SWP_NOZORDER,
).ok();
}
@@ -1225,3 +1419,129 @@ unsafe extern "system" fn destroy_child_window(hwnd: HWND, _: LPARAM) -> BOOL {
unsafe { DestroyWindow(hwnd).ok() };
true.into()
}
/// Download latest Xray-core binary from GitHub releases
fn download_xray_latest() -> Result<String, String> {
use std::fs;
use std::io::Write;
println!("Starting Xray download...");
// Get config directory
let config_dir = crate::config::Config::get_config_path()
.map_err(|e| format!("Failed to get config path: {}", e))?
.parent()
.ok_or("Failed to get config directory")?
.to_path_buf();
// Create xray subdirectory
let xray_dir = config_dir.join("xray");
fs::create_dir_all(&xray_dir)
.map_err(|e| format!("Failed to create xray directory: {}", e))?;
// Fetch latest release page to get actual version
println!("Fetching latest release info...");
let client = reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
let response = client
.get("https://github.com/XTLS/Xray-core/releases/latest")
.send()
.map_err(|e| format!("Failed to fetch latest release: {}", e))?;
// Get redirect location to determine version
let location = response
.headers()
.get("location")
.ok_or("No redirect location found")?
.to_str()
.map_err(|_| "Invalid redirect location")?;
println!("Redirect location: {}", location);
// Extract version from redirect URL (e.g., /XTLS/Xray-core/releases/tag/v25.12.8)
let version = location
.split('/')
.last()
.ok_or("Failed to parse version from redirect")?;
println!("Latest version: {}", version);
// Construct download URL
let download_url = format!(
"https://github.com/XTLS/Xray-core/releases/download/{}/Xray-windows-64.zip",
version
);
println!("Downloading from: {}", download_url);
// Download zip file
let zip_data = reqwest::blocking::get(&download_url)
.map_err(|e| format!("Failed to download zip: {}", e))?
.bytes()
.map_err(|e| format!("Failed to read zip data: {}", e))?;
println!("Downloaded {} bytes", zip_data.len());
// Save zip temporarily
let zip_path = xray_dir.join("xray.zip");
let mut zip_file = fs::File::create(&zip_path)
.map_err(|e| format!("Failed to create zip file: {}", e))?;
zip_file.write_all(&zip_data)
.map_err(|e| format!("Failed to write zip file: {}", e))?;
println!("Extracting zip...");
// Extract zip
let zip_file = fs::File::open(&zip_path)
.map_err(|e| format!("Failed to open zip file: {}", e))?;
let mut archive = zip::ZipArchive::new(zip_file)
.map_err(|e| format!("Failed to open zip archive: {}", e))?;
// Extract all files
for i in 0..archive.len() {
let mut file = archive.by_index(i)
.map_err(|e| format!("Failed to read zip entry: {}", e))?;
let outpath = match file.enclosed_name() {
Some(path) => xray_dir.join(path),
None => continue,
};
if file.name().ends_with('/') {
// Directory
fs::create_dir_all(&outpath)
.map_err(|e| format!("Failed to create directory: {}", e))?;
} else {
// File
if let Some(parent) = outpath.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create parent directory: {}", e))?;
}
let mut outfile = fs::File::create(&outpath)
.map_err(|e| format!("Failed to create file: {}", e))?;
std::io::copy(&mut file, &mut outfile)
.map_err(|e| format!("Failed to extract file: {}", e))?;
println!("Extracted: {}", outpath.display());
}
}
// Delete zip file
let _ = fs::remove_file(&zip_path);
// Find xray.exe in extracted files
let xray_exe = xray_dir.join("xray.exe");
if !xray_exe.exists() {
return Err("xray.exe not found in extracted files".to_string());
}
println!("Xray extracted to: {}", xray_exe.display());
Ok(xray_exe.to_string_lossy().to_string())
}