Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f7edf6df | |||
| ad2ea77a4f |
613
AGENTS.md
Normal file
613
AGENTS.md
Normal file
@@ -0,0 +1,613 @@
|
||||
# AGENTS.md - VPN Tray Manager Development Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Xray-VPN-Manager** is a Windows-only system tray application for managing multiple Xray-core VPN servers from a single subscription URL. Built in Rust with native Windows UI (no web/electron), it provides a lightweight way to enable/disable VPN servers with custom proxy ports.
|
||||
|
||||
**Platform:** Windows 10/11 only (uses Windows API extensively)
|
||||
**Language:** Rust (edition 2024)
|
||||
**Architecture:** Native Win32 GUI + Tokio async runtime for process management
|
||||
|
||||
---
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Build & Run
|
||||
|
||||
```bash
|
||||
# Development build
|
||||
cargo build
|
||||
|
||||
# Release build (optimized)
|
||||
cargo build --release
|
||||
|
||||
# Run (shows console output - #![windows_subsystem = "windows"] commented out in main.rs:1)
|
||||
cargo run
|
||||
|
||||
# Release run (production mode)
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
No test suite currently exists. Manual testing required:
|
||||
1. Run app, check tray icon appears
|
||||
2. Open Settings, enter subscription URL and xray binary path
|
||||
3. Click Update to fetch servers
|
||||
4. Enable servers, set ports/proxy types
|
||||
5. Click Save and verify servers start
|
||||
6. Check tray menu shows running servers
|
||||
|
||||
### Linting & Formatting
|
||||
|
||||
```bash
|
||||
# Check code (standard Rust linting)
|
||||
cargo check
|
||||
|
||||
# Format code
|
||||
cargo fmt
|
||||
|
||||
# Run Clippy for additional lints
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
VPN-Manager/
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, tray icon, Windows message loop
|
||||
│ ├── config.rs # Config persistence (JSON in %APPDATA%)
|
||||
│ ├── xray_manager.rs # Xray process lifecycle management
|
||||
│ ├── vpn/
|
||||
│ │ └── mod.rs # Subscription parsing, URI handling
|
||||
│ └── ui/
|
||||
│ ├── mod.rs # UI module exports
|
||||
│ ├── tray.rs # Tray icon creation, menu rendering
|
||||
│ └── settings_window.rs # Native Win32 settings window (1200+ LOC)
|
||||
├── Cargo.toml # Dependencies, Windows features
|
||||
├── build.rs # Embeds app.manifest via app.rc
|
||||
├── app.manifest # Windows DPI awareness, compatibility
|
||||
└── app.rc # Resource compiler input
|
||||
```
|
||||
|
||||
### Module Responsibilities
|
||||
|
||||
- **main.rs**: Global state (`TOKIO_RUNTIME`, `MENU_UPDATE_REQUESTED`), server restart logic, Windows message pump
|
||||
- **config.rs**: `Config` struct, load/save to `%APPDATA%\Xray-VPN-Manager\config.json`
|
||||
- **xray_manager.rs**: Wraps `v2parser::xray_runner::XrayRunner`, manages server processes in `XRAY_PROCESSES` HashMap
|
||||
- **vpn/mod.rs**: Fetches subscription URLs (base64-encoded), parses URIs (vless, vmess, trojan, ss, socks), assigns local ports
|
||||
- **ui/tray.rs**: Creates tray icon (gold star), builds dynamic menu with running servers
|
||||
- **ui/settings_window.rs**: Complex native Win32 window with custom scrolling, file dialogs, dynamic server list
|
||||
|
||||
---
|
||||
|
||||
## Configuration & Data Flow
|
||||
|
||||
### Config File
|
||||
|
||||
**Location:** `%APPDATA%\Xray-VPN-Manager\config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"subscription_url": "https://example.com/sub",
|
||||
"xray_binary_path": "C:\\path\\to\\xray.exe",
|
||||
"server_settings": {
|
||||
"VLESS://server1.com:443": {
|
||||
"local_port": 1080,
|
||||
"proxy_type": "SOCKS",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server Key Format:** `PROTOCOL://address:port` (e.g., `VLESS://server.com:443`)
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Startup:**
|
||||
- Load config from `%APPDATA%`
|
||||
- Fetch subscription URIs
|
||||
- Parse and assign ports (preserve saved settings)
|
||||
- Store in `VPN_SERVERS` global mutex
|
||||
- Start enabled servers via `xray_manager::start_server()`
|
||||
|
||||
2. **Settings Window:**
|
||||
- User enters/updates subscription URL
|
||||
- Click "Update" → fetch and parse in background thread
|
||||
- Store in `VPN_SERVERS`, post `WM_UPDATE_SERVERS` message
|
||||
- Rebuild server list UI with checkboxes, port edits, proxy type combos
|
||||
- Click "Save" → build `Config` from UI state, save to JSON, call `restart_xray_servers()`
|
||||
|
||||
3. **Server Control:**
|
||||
- `restart_xray_servers()` stops all, starts enabled ones
|
||||
- Updates tray menu via `request_menu_update()` (sets atomic flag)
|
||||
- Main loop checks flag and calls `update_tray_menu()`
|
||||
|
||||
---
|
||||
|
||||
## Code Patterns & Conventions
|
||||
|
||||
### Global State
|
||||
|
||||
Uses `LazyLock` (nightly-stabilized) for lazy static initialization:
|
||||
|
||||
```rust
|
||||
pub static TOKIO_RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
|
||||
tokio::runtime::Runtime::new().expect("Failed to create tokio runtime")
|
||||
});
|
||||
|
||||
pub static VPN_SERVERS: Mutex<Option<Vec<VpnServer>>> = Mutex::new(None);
|
||||
pub static XRAY_PROCESSES: LazyLock<Mutex<HashMap<String, XrayRunner>>> = ...;
|
||||
pub static MENU_UPDATE_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||
```
|
||||
|
||||
### Threading Model
|
||||
|
||||
- **Main thread:** Windows message loop (synchronous, blocking `GetMessageW`)
|
||||
- **Background threads:** Subscription fetching, server start/stop (spawn via `std::thread::spawn`)
|
||||
- **Async runtime:** Tokio runtime for xray process management, accessed via `TOKIO_RUNTIME.block_on(async { ... })`
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Functions return `Result<T, String>` (error messages as strings)
|
||||
- Silent failures common in networking code (vpn/mod.rs): `Err(_) => {}` without logging
|
||||
- Print statements (`println!`, `eprintln!`) for debugging (no structured logging)
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Functions:** `snake_case` (Rust standard)
|
||||
- **Types:** `PascalCase` (`VpnServer`, `ServerSettings`)
|
||||
- **Constants:** `UPPER_SNAKE_CASE` (`ID_URL_EDIT`, `MARGIN`, `WM_UPDATE_SERVERS`)
|
||||
- **Control IDs:** Sequential ranges (checkboxes: 2000+, port edits: 3000+, combos: 4000+, labels: 5000+)
|
||||
|
||||
### Windows API Patterns
|
||||
|
||||
Uses `windows` crate (v0.58) with extensive feature flags:
|
||||
|
||||
```rust
|
||||
#[cfg(windows)]
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::{HWND, LPARAM, WPARAM},
|
||||
UI::WindowsAndMessaging::*,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
- **String encoding:** UTF-16 conversion everywhere: `"text\0".encode_utf16().collect::<Vec<u16>>()`
|
||||
- **Resource management:** No explicit cleanup (relies on OS to clean up on process exit)
|
||||
- **Unsafe:** Almost all Win32 calls wrapped in `unsafe { }` blocks
|
||||
- **Error handling:** `.ok()`, `.unwrap()`, `.expect()` common (crashes on critical failures)
|
||||
|
||||
### UI Control Layout
|
||||
|
||||
Constants define consistent spacing:
|
||||
|
||||
```rust
|
||||
const MARGIN: i32 = 15;
|
||||
const FONT_SIZE: i32 = 32;
|
||||
const LABEL_HEIGHT: i32 = 45;
|
||||
const CONTROL_HEIGHT: i32 = 45;
|
||||
const ROW_HEIGHT: i32 = 55;
|
||||
```
|
||||
|
||||
Dynamic widths calculated from window size in `WM_SIZE` handler.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & External Tools
|
||||
|
||||
### Key Rust Dependencies
|
||||
|
||||
- **tray-icon (0.21):** Cross-platform tray icon (uses Windows native API)
|
||||
- **windows (0.58):** Direct Win32 API bindings (extensive feature list in Cargo.toml)
|
||||
- **tokio (1.x):** Async runtime for process management (rt-multi-thread, sync, macros)
|
||||
- **serde/serde_json (1.x):** Config serialization
|
||||
- **reqwest (0.12):** HTTP client for subscription fetching (blocking feature)
|
||||
- **base64 (0.22):** Decode subscription content
|
||||
- **image (0.25):** Image handling (unused in current code?)
|
||||
- **v2parser (local path):** Custom parser for VPN URIs (path: `../v2-uri-parser`)
|
||||
|
||||
### Critical External Dependency
|
||||
|
||||
**v2parser** is a local crate located at `../v2-uri-parser` (relative to project root). Must exist for project to build.
|
||||
|
||||
Provides:
|
||||
- `v2parser::parser::get_metadata(uri: &str) -> String` (JSON string)
|
||||
- `v2parser::parser::create_json_config(uri, socks_port, http_port) -> String`
|
||||
- `v2parser::xray_runner::XrayRunner` (process management)
|
||||
|
||||
### External Binaries
|
||||
|
||||
**xray-core:** User must download separately from https://github.com/XTLS/Xray-core/releases
|
||||
Binary path configured in settings window and saved to config.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Details
|
||||
|
||||
### Async Runtime Usage
|
||||
|
||||
Tokio runtime is created once at startup and used for:
|
||||
- Starting xray processes: `TOKIO_RUNTIME.block_on(async { xray_manager::start_server(...).await })`
|
||||
- Stopping processes: `TOKIO_RUNTIME.block_on(async { xray_manager::stop_all_servers().await })`
|
||||
|
||||
**Important:** Main thread blocks on async operations (no true concurrency for process management).
|
||||
|
||||
### Process Management
|
||||
|
||||
Each enabled server spawns an xray process via `v2parser::xray_runner::XrayRunner`:
|
||||
- Stored in global `XRAY_PROCESSES: HashMap<server_key, XrayRunner>`
|
||||
- Server key format: `PROTOCOL://address:port`
|
||||
- Processes cleaned up on `stop_server()` or `stop_all_servers()`
|
||||
- All processes stopped on app exit (main.rs:154)
|
||||
|
||||
### Tray Menu Updates
|
||||
|
||||
Two-step update mechanism:
|
||||
1. Call `request_menu_update()` to set `MENU_UPDATE_REQUESTED` atomic flag
|
||||
2. Main loop checks flag and calls `update_tray_menu(tray_icon, settings_item, quit_item)`
|
||||
3. Creates new menu with `create_tray_menu_with_servers()` and sets it via `tray_icon.set_menu()`
|
||||
|
||||
Why? `tray-icon` doesn't support callbacks, and menu must be updated from main thread.
|
||||
|
||||
### Settings Window Lifecycle
|
||||
|
||||
- Created on "Settings" menu item click
|
||||
- HWND stored in `Arc<Mutex<Option<HWND>>>` to prevent duplicates
|
||||
- Brings to front if already open (main.rs:140-151)
|
||||
- Custom window class "SettingsWindowClass" with white background
|
||||
- Custom scroll container class "ScrollContainerClass" with manual scroll handling
|
||||
|
||||
---
|
||||
|
||||
## Common Gotchas
|
||||
|
||||
### 1. Windows-Only Build
|
||||
|
||||
Project **will not compile** on Linux/macOS due to:
|
||||
- `#[cfg(windows)]` everywhere
|
||||
- Direct Win32 API calls
|
||||
- Platform-specific dependencies
|
||||
|
||||
### 2. Edition 2024 Requirement
|
||||
|
||||
`Cargo.toml` specifies `edition = "2024"` (unreleased as of knowledge cutoff). May need Rust nightly or change to `edition = "2021"`.
|
||||
|
||||
### 3. Local Dependency Path
|
||||
|
||||
`v2parser = { path = "../v2-uri-parser" }` must exist at that exact relative path. Not published on crates.io.
|
||||
|
||||
### 4. Windows Subsystem Flag
|
||||
|
||||
`main.rs:1` has `#![windows_subsystem = "windows"]` commented out for debugging. In release:
|
||||
- Uncomment to hide console window
|
||||
- Comment to see stdout/stderr
|
||||
|
||||
### 5. Manifest Embedding
|
||||
|
||||
`build.rs` embeds `app.manifest` via `embed-resource` crate. Required for:
|
||||
- DPI awareness (PerMonitorV2)
|
||||
- Windows 10/11 compatibility flags
|
||||
- Non-admin execution (asInvoker)
|
||||
|
||||
### 6. UTF-16 Null Termination
|
||||
|
||||
All Windows API strings **must** be null-terminated:
|
||||
```rust
|
||||
let text: Vec<u16> = "Hello\0".encode_utf16().collect(); // ✅ Correct
|
||||
let text: Vec<u16> = "Hello".encode_utf16().collect(); // ❌ Crashes or garbage
|
||||
```
|
||||
|
||||
### 7. Control IDs Must Be Unique
|
||||
|
||||
Each window control needs unique ID for `GetDlgItem()`:
|
||||
- Checkboxes: `ID_SERVER_CHECKBOX_BASE + index` (2000+)
|
||||
- Port edits: `ID_SERVER_PORT_EDIT_BASE + index` (3000+)
|
||||
- Combos: `ID_SERVER_PROXY_COMBO_BASE + index` (4000+)
|
||||
|
||||
ID collision causes controls to be unreachable.
|
||||
|
||||
### 8. Custom Message Range
|
||||
|
||||
Custom Windows messages start at `WM_USER + 1`:
|
||||
```rust
|
||||
const WM_UPDATE_SERVERS: u32 = WM_USER + 1;
|
||||
```
|
||||
|
||||
Don't use values below `WM_USER` (conflicts with system messages).
|
||||
|
||||
### 9. Background Thread UI Updates
|
||||
|
||||
Cannot update UI from background threads. Use `PostMessageW()` to marshal to main thread:
|
||||
```rust
|
||||
std::thread::spawn(move || {
|
||||
let servers = fetch_and_process_vpn_list(&url);
|
||||
unsafe {
|
||||
PostMessageW(hwnd, WM_UPDATE_SERVERS, WPARAM(0), LPARAM(0));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 10. Scroll Container Children
|
||||
|
||||
Server list controls are children of scroll container, not main window. Must use:
|
||||
```rust
|
||||
GetDlgItem(container, control_id) // ✅ Correct
|
||||
GetDlgItem(hwnd, control_id) // ❌ Returns error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Approach
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
1. **Tray Icon:**
|
||||
- [ ] Icon appears in system tray (gold star)
|
||||
- [ ] Right-click shows menu (Settings, Exit)
|
||||
- [ ] Exit cleanly stops all processes
|
||||
|
||||
2. **Settings Window:**
|
||||
- [ ] Opens on Settings click
|
||||
- [ ] Focuses if already open (no duplicates)
|
||||
- [ ] URL and Xray path persist from config
|
||||
- [ ] Browse button opens file dialog
|
||||
- [ ] Update fetches servers and populates list
|
||||
|
||||
3. **Server List:**
|
||||
- [ ] Checkboxes toggle enabled state
|
||||
- [ ] Port edits accept numbers only
|
||||
- [ ] Proxy type combo shows SOCKS/HTTP
|
||||
- [ ] Scroll works with mouse wheel and scrollbar
|
||||
- [ ] Window resize adjusts layout
|
||||
|
||||
4. **Save Functionality:**
|
||||
- [ ] Save writes to `%APPDATA%\Xray-VPN-Manager\config.json`
|
||||
- [ ] Servers restart on save
|
||||
- [ ] Tray menu updates with running servers
|
||||
- [ ] Settings persist after app restart
|
||||
|
||||
5. **Error Handling:**
|
||||
- [ ] Invalid URL shows no error (silent failure)
|
||||
- [ ] Missing xray binary path causes start failures (check console)
|
||||
- [ ] Invalid port numbers handled gracefully
|
||||
|
||||
### No Automated Tests
|
||||
|
||||
Currently no unit tests, integration tests, or CI/CD pipeline. All validation is manual.
|
||||
|
||||
---
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Adding a New Menu Item
|
||||
|
||||
1. Create `MenuItem` in `main.rs`:
|
||||
```rust
|
||||
let new_item = MenuItem::new("New Feature", true, None);
|
||||
```
|
||||
|
||||
2. Add to menu in `ui/tray.rs`:
|
||||
```rust
|
||||
tray_menu.append(&new_item).unwrap();
|
||||
```
|
||||
|
||||
3. Handle event in main loop:
|
||||
```rust
|
||||
if event.id == new_item.id() {
|
||||
// Handle click
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a Config Field
|
||||
|
||||
1. Add to `Config` struct in `config.rs`:
|
||||
```rust
|
||||
pub struct Config {
|
||||
pub subscription_url: String,
|
||||
pub xray_binary_path: String,
|
||||
pub new_field: String, // Add this
|
||||
#[serde(default)]
|
||||
pub server_settings: HashMap<String, ServerSettings>,
|
||||
}
|
||||
```
|
||||
|
||||
2. Update `Default` impl:
|
||||
```rust
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
// ...
|
||||
new_field: String::new(),
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Add UI control in `settings_window.rs`:
|
||||
- Define control ID constant
|
||||
- Create control in `create_controls()`
|
||||
- Read value in Save button handler
|
||||
|
||||
### Adding a New VPN Protocol
|
||||
|
||||
1. Update `parse_vpn_uri()` in `vpn/mod.rs`:
|
||||
```rust
|
||||
let is_supported = uri.starts_with("vless://")
|
||||
|| uri.starts_with("newprotocol://"); // Add this
|
||||
```
|
||||
|
||||
2. Ensure `v2parser` crate supports the protocol (external dependency).
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Enable Console Output
|
||||
|
||||
Ensure `#![windows_subsystem = "windows"]` is **commented** in `main.rs:1`.
|
||||
|
||||
### Common Debug Points
|
||||
|
||||
1. **Subscription fetch fails:**
|
||||
- Check URL is valid and returns base64-encoded content
|
||||
- Add `println!` in `fetch_and_process_vpn_list()` to see response
|
||||
|
||||
2. **Servers don't start:**
|
||||
- Verify xray binary path exists
|
||||
- Check `xray_manager::start_server()` errors in console
|
||||
- Ensure `v2parser` generates valid config
|
||||
|
||||
3. **Menu doesn't update:**
|
||||
- Verify `request_menu_update()` called
|
||||
- Check `MENU_UPDATE_REQUESTED` flag in main loop
|
||||
- Ensure `VPN_SERVERS` mutex populated
|
||||
|
||||
4. **Settings window controls missing:**
|
||||
- Check control IDs are unique
|
||||
- Verify `GetDlgItem()` uses correct parent (container vs. hwnd)
|
||||
- Look for "Failed to create..." messages
|
||||
|
||||
5. **DPI scaling issues:**
|
||||
- Verify `app.manifest` embedded (check `build.rs` runs)
|
||||
- Ensure `SetProcessDpiAwarenessContext` called at startup
|
||||
|
||||
### Useful Print Statements
|
||||
|
||||
```rust
|
||||
println!("Settings window created: {:?}", hwnd);
|
||||
println!("URL entered: {}", url);
|
||||
println!("Started server: {}", server.name);
|
||||
eprintln!("Failed to start server {}: {}", server.name, e);
|
||||
```
|
||||
|
||||
Already present in code for basic debugging.
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### UI Thread Blocking
|
||||
|
||||
Main thread blocks on:
|
||||
- `GetMessageW()` (Windows message pump)
|
||||
- `TOKIO_RUNTIME.block_on()` (async operations)
|
||||
|
||||
Keep async operations fast to avoid UI freezes.
|
||||
|
||||
### Scroll Performance
|
||||
|
||||
Settings window with many servers (100+) may lag:
|
||||
- Each server creates 4-5 controls (checkbox, label, edit, combo)
|
||||
- Scroll handler moves all children on every scroll event
|
||||
- Consider virtualization if server count exceeds ~50
|
||||
|
||||
### Subscription Fetch
|
||||
|
||||
Blocking HTTP call in background thread:
|
||||
```rust
|
||||
reqwest::blocking::get(url)
|
||||
```
|
||||
|
||||
No timeout configured. May hang on slow/unresponsive servers.
|
||||
|
||||
---
|
||||
|
||||
## Security & Safety
|
||||
|
||||
### Unsafe Code
|
||||
|
||||
Extensive use of `unsafe` for Windows API:
|
||||
- String pointer conversions (`PCWSTR::from_raw()`)
|
||||
- Window handles (`HWND` casting)
|
||||
- Message parameter packing (`WPARAM`, `LPARAM`)
|
||||
|
||||
**Assumption:** Windows API contracts upheld (e.g., null-terminated strings).
|
||||
|
||||
### Process Execution
|
||||
|
||||
Executes arbitrary xray binary from user-configured path. No validation of binary integrity.
|
||||
|
||||
### Network Requests
|
||||
|
||||
Fetches subscription URLs without TLS verification control. Uses `reqwest` defaults (should verify certificates).
|
||||
|
||||
### Config Storage
|
||||
|
||||
Plaintext JSON in `%APPDATA%`. No encryption. Contains:
|
||||
- Subscription URLs (may include credentials in query params)
|
||||
- Server addresses/ports
|
||||
- Local proxy ports
|
||||
|
||||
**Not suitable for highly sensitive credentials.**
|
||||
|
||||
---
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Based on code analysis:
|
||||
|
||||
1. **Add tests:** Unit tests for config, integration tests for subscription parsing
|
||||
2. **Logging:** Replace `println!` with structured logging (e.g., `tracing`, `env_logger`)
|
||||
3. **Error handling:** Return structured errors instead of `String`, display errors in UI
|
||||
4. **Timeouts:** Add request timeouts to `reqwest::blocking::get()`
|
||||
5. **Virtualized list:** For 50+ servers, implement virtual scrolling
|
||||
6. **Tray menu callbacks:** Investigate better tray update mechanism (polling is suboptimal)
|
||||
7. **CI/CD:** Add GitHub Actions for Windows builds
|
||||
8. **Installer:** Package as MSI/NSIS installer instead of bare `.exe`
|
||||
9. **Auto-update:** Check for new versions on startup
|
||||
10. **Connection testing:** Ping/test servers before enabling
|
||||
|
||||
---
|
||||
|
||||
## Build Troubleshooting
|
||||
|
||||
### "edition 2024 not found"
|
||||
|
||||
Change `Cargo.toml`:
|
||||
```toml
|
||||
edition = "2021" # Change from 2024
|
||||
```
|
||||
|
||||
### "v2parser not found"
|
||||
|
||||
Ensure `../v2-uri-parser` exists relative to project root. Clone or create it separately.
|
||||
|
||||
### "embed-resource failed"
|
||||
|
||||
Ensure Windows SDK installed (required for `rc.exe` resource compiler).
|
||||
|
||||
### Missing Windows features
|
||||
|
||||
If compile fails with missing types, add to `Cargo.toml`:
|
||||
```toml
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
# Add missing feature here
|
||||
] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary for AI Agents
|
||||
|
||||
When working in this codebase:
|
||||
|
||||
1. **Platform:** Windows-only, will not compile elsewhere
|
||||
2. **External dep:** `v2parser` at `../v2-uri-parser` required
|
||||
3. **Testing:** Manual only, no automated tests
|
||||
4. **UI:** Native Win32, complex custom controls in `settings_window.rs`
|
||||
5. **Async:** Tokio runtime used, but main thread blocks on operations
|
||||
6. **State:** Global mutexes (`VPN_SERVERS`, `XRAY_PROCESSES`) shared across threads
|
||||
7. **Config:** `%APPDATA%\Xray-VPN-Manager\config.json`, plaintext JSON
|
||||
8. **Strings:** Always UTF-16 with null terminator for Windows API
|
||||
9. **IDs:** Control IDs must be unique, use defined constants + index
|
||||
10. **Updates:** UI updates from background threads via `PostMessageW()`
|
||||
|
||||
**Most complex file:** `ui/settings_window.rs` (1200+ lines, custom scrolling, dynamic layout)
|
||||
**Most critical function:** `restart_xray_servers()` in `main.rs` (stops/starts all servers)
|
||||
**Most fragile part:** Windows API unsafe code (crashes if assumptions violated)
|
||||
370
Cargo.lock
generated
370
Cargo.lock
generated
@@ -2,12 +2,40 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "Xray-VPN-Manager"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"embed-resource",
|
||||
"image",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tray-icon",
|
||||
"v2parser",
|
||||
"windows",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -96,6 +124,9 @@ name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arg_enum_proc_macro"
|
||||
@@ -237,6 +268,15 @@ dependencies = [
|
||||
"core2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
@@ -264,6 +304,12 @@ version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
@@ -276,6 +322,25 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.5"
|
||||
@@ -329,6 +394,16 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
@@ -381,6 +456,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -406,6 +487,30 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -455,6 +560,53 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
@@ -821,6 +973,16 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
@@ -839,9 +1001,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1044,6 +1208,15 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -1315,6 +1488,15 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
@@ -1515,6 +1697,27 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maybe-rayon"
|
||||
version = "0.1.1"
|
||||
@@ -1646,6 +1849,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
@@ -1868,6 +2077,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -1927,6 +2146,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -2438,6 +2663,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -2679,6 +2915,25 @@ dependencies = [
|
||||
"zune-jpeg 0.4.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
@@ -2886,6 +3141,12 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
@@ -3100,22 +3361,6 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "Xray-VPN-Manager"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"embed-resource",
|
||||
"image",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tray-icon",
|
||||
"v2parser",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -3500,6 +3745,15 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y4m"
|
||||
version = "0.8.0"
|
||||
@@ -3575,6 +3829,20 @@ name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
@@ -3609,12 +3877,82 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"indexmap",
|
||||
"lzma-rs",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"xz2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "Xray-VPN-Manager"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -10,8 +10,10 @@ reqwest = { version = "0.12", features = ["blocking"] }
|
||||
base64 = "0.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
v2parser = { path = "../v2-uri-parser" }
|
||||
#v2parser = { path = "../v2-uri-parser" }
|
||||
v2parser = { git = "https://github.com/house-of-vanity/v2-uri-parser.git" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] }
|
||||
zip = "2.2"
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "2.5"
|
||||
@@ -27,4 +29,5 @@ windows = { version = "0.58", features = [
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Registry",
|
||||
] }
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user