- 新增发布到GitHub功能(scan/init/commit/push/register全流程) - 新增WSL发行版检测与UNC路径自动转换,路径存储改为Windows可访问格式 - 修复libgit2访问WSL UNC路径的owner验证问题(启动时全局禁用) - ProjectCard新增本地项目/本地仓库/远程仓库三灯状态指示 - ProjectModal重构为基本信息+部署信息双标签页,固定高度稳定切换 - 添加项目支持顶部快速选择文件夹并自动填充ID和名称 - 设置页WSL区块改为下拉检测选择发行版 - Dialog组件支持fixedHeight固定高度模式
371 lines
12 KiB
Rust
371 lines
12 KiB
Rust
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)
|
||
}
|