dev-manager-tauri/src-tauri/src/commands/network.rs
lanrtop 0baa304fbe feat: WSL路径支持、项目发布到GitHub、三灯状态指示
- 新增发布到GitHub功能(scan/init/commit/push/register全流程)
- 新增WSL发行版检测与UNC路径自动转换,路径存储改为Windows可访问格式
- 修复libgit2访问WSL UNC路径的owner验证问题(启动时全局禁用)
- ProjectCard新增本地项目/本地仓库/远程仓库三灯状态指示
- ProjectModal重构为基本信息+部署信息双标签页,固定高度稳定切换
- 添加项目支持顶部快速选择文件夹并自动填充ID和名称
- 设置页WSL区块改为下拉检测选择发行版
- Dialog组件支持fixedHeight固定高度模式
2026-04-01 01:36:21 +09:00

371 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::db::pool;
use rusqlite::params;
use serde::{Deserialize, Serialize};
use std::process::Command;
use uuid::Uuid;
// ── 数据结构 ──────────────────────────────────────────────────────────────────
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MonitoredTool {
pub id: String,
pub name: String,
pub exe_path: String,
pub enabled: bool,
}
#[derive(Debug, Serialize, Clone)]
pub struct ConnectionInfo {
pub process_name: String,
pub process_path: String,
pub remote_address: String,
pub remote_port: u16,
}
#[derive(Debug, Serialize, Clone)]
pub struct ModeLogEntry {
pub id: i64,
pub mode: String,
pub created_at: Option<String>,
}
// ── 防火墙规则管理 ────────────────────────────────────────────────────────────
fn rule_display_name(tool_name: &str) -> String {
format!("DevMgr_Block_{}", tool_name.replace(' ', "_"))
}
fn run_ps(script: &str) -> Result<String, String> {
let output = Command::new("powershell")
.args(["-NonInteractive", "-NoProfile", "-Command", script])
.output()
.map_err(|e| format!("无法启动 PowerShell: {e}"))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(err)
}
}
/// 添加出站封锁规则
fn firewall_block(tool_name: &str, exe_path: &str) -> Result<(), String> {
let name = rule_display_name(tool_name);
// 先尝试删除旧规则(幂等)
let _ = run_ps(&format!(
"Remove-NetFirewallRule -DisplayName '{name}' -ErrorAction SilentlyContinue"
));
run_ps(&format!(
"New-NetFirewallRule -DisplayName '{name}' -Direction Outbound \
-Program '{exe_path}' -Action Block -Profile Any -ErrorAction Stop"
))
.map(|_| ())
.map_err(|e| {
if e.contains("access") || e.contains("denied") || e.contains("privilege") || e.contains("Privilege") {
"需要管理员权限才能修改防火墙规则,请以管理员身份运行本程序".to_string()
} else {
e
}
})
}
/// 删除出站封锁规则(恢复放行)
fn firewall_unblock(tool_name: &str) -> Result<(), String> {
let name = rule_display_name(tool_name);
run_ps(&format!(
"Remove-NetFirewallRule -DisplayName '{name}' -ErrorAction SilentlyContinue"
))
.map(|_| ())
.map_err(|e| {
if e.contains("access") || e.contains("denied") || e.contains("privilege") || e.contains("Privilege") {
"需要管理员权限才能修改防火墙规则,请以管理员身份运行本程序".to_string()
} else {
e
}
})
}
// ── Tauri 命令 ────────────────────────────────────────────────────────────────
/// 获取当前开发模式状态
#[tauri::command]
pub fn get_dev_mode() -> Result<bool, String> {
let conn = pool().get().map_err(|e| e.to_string())?;
let val: Option<String> = conn
.query_row(
"SELECT value FROM settings WHERE key = 'dev_mode'",
[],
|row| row.get(0),
)
.ok();
// 默认为 false(非开发模式)
Ok(val.as_deref() == Some("1"))
}
/// 切换开发模式(同时应用/撤销防火墙规则)
#[tauri::command]
pub fn set_dev_mode(enabled: bool) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
// 获取所有已启用的受监控工具
let mut stmt = conn
.prepare("SELECT name, exe_path FROM network_monitored_tools WHERE enabled = 1")
.map_err(|e| e.to_string())?;
let tools: Vec<(String, String)> = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
// 应用防火墙规则
let mut errors: Vec<String> = Vec::new();
for (name, path) in &tools {
let result = if enabled {
firewall_unblock(name)
} else {
firewall_block(name, path)
};
if let Err(e) = result {
errors.push(format!("{name}: {e}"));
break; // 权限错误无需继续尝试
}
}
if !errors.is_empty() {
return Err(errors.join("\n"));
}
// 保存模式到设置
conn.execute(
"INSERT INTO settings (key, value) VALUES ('dev_mode', ?1)
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
params![if enabled { "1" } else { "0" }],
)
.map_err(|e| e.to_string())?;
// 记录切换历史
conn.execute(
"INSERT INTO network_mode_log (mode, created_at) VALUES (?1, datetime('now'))",
params![if enabled { "dev" } else { "non_dev" }],
)
.map_err(|e| e.to_string())?;
Ok(())
}
/// 获取受监控工具列表
#[tauri::command]
pub fn get_monitored_tools() -> Result<Vec<MonitoredTool>, String> {
let conn = pool().get().map_err(|e| e.to_string())?;
let mut stmt = conn
.prepare(
"SELECT id, name, exe_path, enabled FROM network_monitored_tools ORDER BY name",
)
.map_err(|e| e.to_string())?;
let tools = stmt
.query_map([], |row| {
Ok(MonitoredTool {
id: row.get(0)?,
name: row.get(1)?,
exe_path: row.get(2)?,
enabled: row.get::<_, i64>(3)? == 1,
})
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(tools)
}
/// 添加受监控工具
#[tauri::command]
pub fn add_monitored_tool(name: String, exe_path: String) -> Result<String, String> {
let id = Uuid::new_v4().to_string();
let conn = pool().get().map_err(|e| e.to_string())?;
conn.execute(
"INSERT INTO network_monitored_tools (id, name, exe_path, enabled) VALUES (?1, ?2, ?3, 1)",
params![id, name, exe_path],
)
.map_err(|e| e.to_string())?;
// 如果当前是非开发模式,立即对新工具应用封锁规则
let dev_mode: bool = conn
.query_row("SELECT value FROM settings WHERE key = 'dev_mode'", [], |row| row.get::<_, String>(0))
.ok()
.as_deref()
== Some("1");
if !dev_mode {
let _ = firewall_block(&name, &exe_path); // 非关键失败,忽略
}
Ok(id)
}
/// 移除受监控工具(同时清除其防火墙规则)
#[tauri::command]
pub fn remove_monitored_tool(id: String) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
// 先查出名称,用于删除防火墙规则
let name: Option<String> = conn
.query_row(
"SELECT name FROM network_monitored_tools WHERE id = ?1",
params![id],
|row| row.get(0),
)
.ok();
conn.execute(
"DELETE FROM network_monitored_tools WHERE id = ?1",
params![id],
)
.map_err(|e| e.to_string())?;
// 清除对应防火墙规则(不管当前是什么模式都清理干净)
if let Some(n) = name {
let _ = firewall_unblock(&n);
}
Ok(())
}
/// 获取当前活跃的出站网络连接(只返回受监控工具相关的进程)
#[tauri::command]
pub fn get_active_connections() -> Result<Vec<ConnectionInfo>, String> {
// 获取受监控工具的进程名(exe 文件名,不含路径)
let conn = pool().get().map_err(|e| e.to_string())?;
let mut stmt = conn
.prepare("SELECT exe_path FROM network_monitored_tools WHERE enabled = 1")
.map_err(|e| e.to_string())?;
let paths: Vec<String> = stmt
.query_map([], |row| row.get(0))
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
if paths.is_empty() {
return Ok(vec![]);
}
// 提取文件名(不含后缀)用于匹配进程名
let process_names: Vec<String> = paths
.iter()
.filter_map(|p| {
std::path::Path::new(p)
.file_stem()
.map(|s| s.to_string_lossy().to_lowercase())
})
.collect();
// PowerShell 脚本:获取已建立的出站连接,每行 "进程名||远端IP|端口"
// 不读取 MainModule.FileName(极慢,需枚举每个进程的模块列表,且对系统进程易超时)
let ps_script = r#"
$results = @()
try {
Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
Where-Object { $_.RemoteAddress -notmatch '^(0\.0\.0\.0|127\.|::1$|::$)' } |
ForEach-Object {
$proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
if ($proc) {
$results += "$($proc.ProcessName)||$($_.RemoteAddress)|$($_.RemotePort)"
}
}
} catch {}
$results -join "`n"
"#;
let output = run_ps(ps_script).unwrap_or_default();
let mut connections: Vec<ConnectionInfo> = Vec::new();
for line in output.lines() {
let parts: Vec<&str> = line.splitn(4, '|').collect();
if parts.len() < 4 { continue; }
let proc_name = parts[0].to_lowercase();
// 只返回受监控工具的连接
if !process_names.iter().any(|n| proc_name.contains(n.as_str())) {
continue;
}
let port: u16 = parts[3].parse().unwrap_or(0);
connections.push(ConnectionInfo {
process_name: parts[0].to_string(),
process_path: parts[1].to_string(),
remote_address: parts[2].to_string(),
remote_port: port,
});
}
Ok(connections)
}
// ── WSL v2rayA 代理控制 ────────────────────────────────────────────────────────
fn run_wsl_root(cmd: &str) -> Result<std::process::Output, String> {
std::process::Command::new("wsl")
.args(["-u", "root", "-e", "bash", "-c", cmd])
.output()
.map_err(|e| format!("WSL 不可用: {e}"))
}
/// 获取 v2rayA 运行状态:"running" | "stopped" | "wsl_unavailable"
#[tauri::command]
pub fn get_v2raya_status() -> Result<String, String> {
let output = run_wsl_root("pgrep -x v2raya > /dev/null 2>&1 && echo running || echo stopped")
.map_err(|_| "wsl_unavailable".to_string())?;
let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(if out == "running" {
"running".to_string()
} else {
"stopped".to_string()
})
}
/// 启动 v2rayA(后台运行,不阻塞 WSL 会话)
#[tauri::command]
pub fn start_v2raya() -> Result<(), String> {
let output = run_wsl_root(
"nohup v2raya --log-disable-color > /tmp/v2raya.log 2>&1 & sleep 1; \
pgrep -x v2raya > /dev/null && echo ok || echo fail",
)?;
let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
if out.contains("ok") {
Ok(())
} else {
Err("v2rayA 启动失败,请确认已在 WSL 中安装(apt install v2raya)".to_string())
}
}
/// 停止 v2rayA
#[tauri::command]
pub fn stop_v2raya() -> Result<(), String> {
run_wsl_root("pkill v2raya; exit 0")?;
Ok(())
}
/// 获取模式切换历史
#[tauri::command]
pub fn get_mode_log(limit: Option<i64>) -> Result<Vec<ModeLogEntry>, String> {
let conn = pool().get().map_err(|e| e.to_string())?;
let n = limit.unwrap_or(20);
let mut stmt = conn
.prepare(
"SELECT id, mode, created_at FROM network_mode_log ORDER BY created_at DESC LIMIT ?1",
)
.map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![n], |row| {
Ok(ModeLogEntry {
id: row.get(0)?,
mode: row.get(1)?,
created_at: row.get(2)?,
})
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(rows)
}