Release version 1.2 with Supernode user window
Build Windows Portable Package / portable (release) Successful in 9m52s
Build Windows Portable Package / portable (release) Successful in 9m52s
This commit is contained in:
+511
-22
@@ -3,10 +3,14 @@ use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
net::{Ipv4Addr, SocketAddr, TcpStream, UdpSocket},
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream, ToSocketAddrs, UdpSocket},
|
||||
path::PathBuf,
|
||||
process::{Child, Command, Stdio},
|
||||
sync::Mutex,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri::{
|
||||
@@ -17,9 +21,70 @@ use tauri::{
|
||||
|
||||
struct AppState {
|
||||
processes: Mutex<HashMap<String, Child>>,
|
||||
special_server: Mutex<Option<SpecialServerHandle>>,
|
||||
}
|
||||
|
||||
struct SpecialServerHandle {
|
||||
stop: Arc<AtomicBool>,
|
||||
join: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SupernodeRequest {
|
||||
port: u16,
|
||||
dhcp: bool,
|
||||
dhcp_pool: String,
|
||||
allowed_communities: String,
|
||||
special_enabled: bool,
|
||||
special_port: u16,
|
||||
special_community: String,
|
||||
special_pool: String,
|
||||
special_gateway: String,
|
||||
special_dns: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SpecialClientRequest {
|
||||
host: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SpecialConfig {
|
||||
community: String,
|
||||
ip: String,
|
||||
gateway: String,
|
||||
dns: String,
|
||||
supernode_port: u16,
|
||||
username: String,
|
||||
special: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SpecialUser {
|
||||
username: String,
|
||||
ip: String,
|
||||
special: bool,
|
||||
last_seen: u64,
|
||||
}
|
||||
|
||||
struct SpecialServerData {
|
||||
community: String,
|
||||
pool_start: u32,
|
||||
pool_end: u32,
|
||||
gateway: String,
|
||||
dns: String,
|
||||
supernode_port: u16,
|
||||
leases: Mutex<HashMap<String, SpecialUser>>,
|
||||
}
|
||||
|
||||
fn stop_all_processes(state: &AppState) {
|
||||
stop_special_server(state);
|
||||
let Ok(mut processes) = state.processes.lock() else {
|
||||
return;
|
||||
};
|
||||
@@ -52,6 +117,223 @@ fn tray_icon_image() -> tauri::image::Image<'static> {
|
||||
tauri::image::Image::new_owned(rgba, 16, 16)
|
||||
}
|
||||
|
||||
fn parse_cidr(value: &str) -> Result<(u32, u32, u8), String> {
|
||||
let (address, prefix) = value
|
||||
.trim()
|
||||
.split_once('/')
|
||||
.ok_or_else(|| format!("地址池必须使用 CIDR 格式:{value}"))?;
|
||||
let ip = address
|
||||
.parse::<Ipv4Addr>()
|
||||
.map_err(|_| format!("无效的地址池:{value}"))?;
|
||||
let prefix = prefix
|
||||
.parse::<u8>()
|
||||
.map_err(|_| format!("无效的 CIDR 前缀:{value}"))?;
|
||||
if prefix > 30 {
|
||||
return Err("地址池前缀必须在 0 到 30 之间".to_string());
|
||||
}
|
||||
let bits = u32::from(ip);
|
||||
let mask = if prefix == 0 {
|
||||
0
|
||||
} else {
|
||||
u32::MAX << (32 - prefix)
|
||||
};
|
||||
let network = bits & mask;
|
||||
Ok((network, network | !mask, prefix))
|
||||
}
|
||||
|
||||
fn dotted_ip(value: u32) -> String {
|
||||
Ipv4Addr::from(value).to_string()
|
||||
}
|
||||
|
||||
fn cidr_range(value: &str) -> Result<String, String> {
|
||||
let (start, _end, prefix) = parse_cidr(value)?;
|
||||
Ok(format!(
|
||||
"{}-{}/{}",
|
||||
dotted_ip(start),
|
||||
dotted_ip(start),
|
||||
prefix
|
||||
))
|
||||
}
|
||||
|
||||
fn validate_supernode_request(request: &SupernodeRequest) -> Result<(), String> {
|
||||
if !(1..=65535).contains(&request.port) {
|
||||
return Err("Supernode UDP 端口必须在 1 到 65535 之间".to_string());
|
||||
}
|
||||
let base_pool = if request.dhcp {
|
||||
Some(parse_cidr(&request.dhcp_pool)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if request.special_enabled {
|
||||
if !(1..=65535).contains(&request.special_port) {
|
||||
return Err("特殊服务端 TCP 端口必须在 1 到 65535 之间".to_string());
|
||||
}
|
||||
if request.special_port == request.port {
|
||||
return Err("特殊服务端 TCP 端口不能与 Supernode UDP 端口相同".to_string());
|
||||
}
|
||||
if request.special_community.trim().is_empty() {
|
||||
return Err("请填写特殊服务端下发的社区名".to_string());
|
||||
}
|
||||
let special_pool = parse_cidr(&request.special_pool)?;
|
||||
let Some(base_pool) = base_pool else {
|
||||
return Err("开启特殊服务端时必须开启 DHCP,并配置普通 DHCP 地址池".to_string());
|
||||
};
|
||||
if special_pool.2 < base_pool.2
|
||||
|| special_pool.0 < base_pool.0
|
||||
|| special_pool.1 > base_pool.1
|
||||
{
|
||||
return Err("特殊服务端地址池必须包含在普通 DHCP 地址池内".to_string());
|
||||
}
|
||||
let allowed_lines = request
|
||||
.allowed_communities
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if !allowed_lines.is_empty()
|
||||
&& !allowed_lines
|
||||
.iter()
|
||||
.any(|line| *line == request.special_community.trim())
|
||||
{
|
||||
return Err("特殊服务端社区名必须出现在允许的社区名列表中".to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_special_server(state: &AppState) {
|
||||
let handle = state
|
||||
.special_server
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut value| value.take());
|
||||
if let Some(mut handle) = handle {
|
||||
handle.stop.store(true, Ordering::Relaxed);
|
||||
if let Some(join) = handle.join.take() {
|
||||
let _ = join.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate_special_ip(data: &SpecialServerData) -> Option<String> {
|
||||
let leases = data.leases.lock().ok()?;
|
||||
let used = leases
|
||||
.values()
|
||||
.map(|lease| lease.ip.clone())
|
||||
.collect::<Vec<_>>();
|
||||
(data.pool_start..=data.pool_end)
|
||||
.filter(|ip| *ip != data.pool_start && *ip != data.pool_end)
|
||||
.map(dotted_ip)
|
||||
.find(|ip| !used.iter().any(|used_ip| used_ip == ip))
|
||||
}
|
||||
|
||||
fn handle_special_connection(mut stream: TcpStream, data: &SpecialServerData) {
|
||||
let mut command = String::new();
|
||||
if BufReader::new(&mut stream).read_line(&mut command).is_err() {
|
||||
return;
|
||||
}
|
||||
let command = command.trim();
|
||||
if let Some(username) = command.strip_prefix("HELLO ") {
|
||||
let username = if username.trim().is_empty() {
|
||||
"未命名客户端".to_string()
|
||||
} else {
|
||||
username.trim().chars().take(64).collect()
|
||||
};
|
||||
let ip = {
|
||||
let existing = data
|
||||
.leases
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|leases| leases.get(&username).map(|lease| lease.ip.clone()));
|
||||
existing.or_else(|| allocate_special_ip(data))
|
||||
};
|
||||
let Some(ip) = ip else {
|
||||
let _ = writeln!(stream, "{{\"error\":\"地址池已耗尽\"}}");
|
||||
return;
|
||||
};
|
||||
if let Ok(mut leases) = data.leases.lock() {
|
||||
leases.insert(
|
||||
username.clone(),
|
||||
SpecialUser {
|
||||
username: username.clone(),
|
||||
ip: ip.clone(),
|
||||
special: true,
|
||||
last_seen: now_epoch(),
|
||||
},
|
||||
);
|
||||
}
|
||||
let response = SpecialConfig {
|
||||
community: data.community.clone(),
|
||||
ip,
|
||||
gateway: data.gateway.clone(),
|
||||
dns: data.dns.clone(),
|
||||
supernode_port: data.supernode_port,
|
||||
username,
|
||||
special: true,
|
||||
};
|
||||
if let Ok(body) = serde_json::to_string(&response) {
|
||||
let _ = writeln!(stream, "{body}");
|
||||
}
|
||||
} else if command == "LIST" {
|
||||
let now = now_epoch();
|
||||
let users = data
|
||||
.leases
|
||||
.lock()
|
||||
.map(|leases| {
|
||||
leases
|
||||
.values()
|
||||
.filter(|lease| now.saturating_sub(lease.last_seen) <= 90)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if let Ok(body) = serde_json::to_string(&users) {
|
||||
let _ = writeln!(stream, "{body}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_special_server(request: &SupernodeRequest, state: &AppState) -> Result<(), String> {
|
||||
stop_special_server(state);
|
||||
let (pool_start, pool_end, _) = parse_cidr(&request.special_pool)?;
|
||||
let listener = TcpListener::bind(("0.0.0.0", request.special_port))
|
||||
.map_err(|error| format!("特殊服务端无法监听 TCP {}:{error}", request.special_port))?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.map_err(|error| format!("特殊服务端设置监听模式失败:{error}"))?;
|
||||
let data = Arc::new(SpecialServerData {
|
||||
community: request.special_community.trim().to_string(),
|
||||
pool_start,
|
||||
pool_end,
|
||||
gateway: request.special_gateway.trim().to_string(),
|
||||
dns: request.special_dns.trim().to_string(),
|
||||
supernode_port: request.port,
|
||||
leases: Mutex::new(HashMap::new()),
|
||||
});
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let thread_stop = Arc::clone(&stop);
|
||||
let join = std::thread::spawn(move || {
|
||||
while !thread_stop.load(Ordering::Relaxed) {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => handle_special_connection(stream, &data),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
state
|
||||
.special_server
|
||||
.lock()
|
||||
.map_err(|_| "特殊服务端状态不可用".to_string())?
|
||||
.replace(SpecialServerHandle {
|
||||
stop,
|
||||
join: Some(join),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StartRequest {
|
||||
@@ -340,11 +622,7 @@ fn windows_tap_adapters() -> Result<Vec<TapAdapter>, String> {
|
||||
Ok(adapters)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_service(
|
||||
request: StartRequest,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ServiceStatus, String> {
|
||||
fn start_service_inner(request: StartRequest, state: &AppState) -> Result<ServiceStatus, String> {
|
||||
let mut processes = state
|
||||
.processes
|
||||
.lock()
|
||||
@@ -409,12 +687,147 @@ fn start_service(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_service(
|
||||
request: StartRequest,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ServiceStatus, String> {
|
||||
start_service_inner(request, state.inner())
|
||||
}
|
||||
|
||||
fn stop_service_process(service: &str, state: &AppState) -> ServiceStatus {
|
||||
let Ok(mut processes) = state.processes.lock() else {
|
||||
return empty_status(service, "process state is unavailable");
|
||||
};
|
||||
if let Some(mut child) = processes.remove(service) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return empty_status(service, "stopped");
|
||||
}
|
||||
empty_status(service, "not running")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_supernode(
|
||||
request: SupernodeRequest,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ServiceStatus, String> {
|
||||
validate_supernode_request(&request)?;
|
||||
let directory = app_directory()?;
|
||||
let allowed_path = directory.join("supernode-communities.list");
|
||||
let allowed = request
|
||||
.allowed_communities
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if allowed.is_empty() {
|
||||
let _ = fs::remove_file(&allowed_path);
|
||||
} else {
|
||||
fs::write(&allowed_path, format!("{}\n", allowed.join("\n")))
|
||||
.map_err(|error| format!("无法写入社区列表:{error}"))?;
|
||||
}
|
||||
let mut config = format!("-p {}\n", request.port);
|
||||
if request.dhcp {
|
||||
config.push_str(&format!("-a {}\n", cidr_range(&request.dhcp_pool)?));
|
||||
}
|
||||
if !allowed.is_empty() {
|
||||
config.push_str("-c supernode-communities.list\n");
|
||||
}
|
||||
config.push_str("-v\n");
|
||||
fs::write(directory.join("supernode.conf"), config)
|
||||
.map_err(|error| format!("无法写入 Supernode 配置:{error}"))?;
|
||||
|
||||
let status = start_service_inner(
|
||||
StartRequest {
|
||||
service: "supernode".to_string(),
|
||||
binary_path: "./n2n/supernode.exe".to_string(),
|
||||
args: vec!["supernode.conf".to_string()],
|
||||
},
|
||||
state.inner(),
|
||||
)?;
|
||||
if request.special_enabled {
|
||||
if let Err(error) = start_special_server(&request, state.inner()) {
|
||||
stop_service_process("supernode", state.inner());
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_special_service(state: State<'_, AppState>) -> Result<(), String> {
|
||||
stop_special_server(state.inner());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn special_request(request: &SpecialClientRequest, command: &str) -> Result<String, String> {
|
||||
if request.host.trim().is_empty() || !(1..=65535).contains(&request.port) {
|
||||
return Err("特殊服务端地址或端口无效".to_string());
|
||||
}
|
||||
let address = format!("{}:{}", request.host.trim(), request.port);
|
||||
let socket = address
|
||||
.to_socket_addrs()
|
||||
.map_err(|error| format!("无法解析特殊服务端地址:{error}"))?
|
||||
.next()
|
||||
.ok_or_else(|| format!("无效的特殊服务端地址:{address}"))?;
|
||||
let mut stream = TcpStream::connect_timeout(&socket, Duration::from_secs(3))
|
||||
.map_err(|error| format!("无法连接特殊服务端:{error}"))?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(3)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
writeln!(stream, "{command}").map_err(|error| format!("发送特殊服务端请求失败:{error}"))?;
|
||||
let mut response = String::new();
|
||||
BufReader::new(stream)
|
||||
.read_line(&mut response)
|
||||
.map_err(|error| format!("读取特殊服务端响应失败:{error}"))?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn default_username() -> String {
|
||||
std::env::var("COMPUTERNAME")
|
||||
.or_else(|_| std::env::var("HOSTNAME"))
|
||||
.unwrap_or_else(|_| "未命名客户端".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn fetch_special_config(request: SpecialClientRequest) -> Result<SpecialConfig, String> {
|
||||
let username = if request.username.trim().is_empty() {
|
||||
default_username()
|
||||
} else {
|
||||
request.username.trim().to_string()
|
||||
};
|
||||
let command = format!("HELLO {}", username.chars().take(64).collect::<String>());
|
||||
let response = special_request(&request, &command)?;
|
||||
if let Ok(error) = serde_json::from_str::<HashMap<String, String>>(&response) {
|
||||
if let Some(message) = error.get("error") {
|
||||
return Err(message.clone());
|
||||
}
|
||||
}
|
||||
serde_json::from_str(response.trim())
|
||||
.map_err(|error| format!("特殊服务端返回无效配置:{error}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn fetch_special_users(request: SpecialClientRequest) -> Result<Vec<SpecialUser>, String> {
|
||||
if !request.username.trim().is_empty() {
|
||||
let username = request.username.trim().chars().take(64).collect::<String>();
|
||||
let _ = special_request(&request, &format!("HELLO {username}"))?;
|
||||
}
|
||||
let response = special_request(&request, "LIST")?;
|
||||
serde_json::from_str(response.trim()).map_err(|error| format!("在线用户列表无效:{error}"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TapAddressRequest {
|
||||
tap_device: String,
|
||||
dhcp: bool,
|
||||
ip: String,
|
||||
#[serde(default)]
|
||||
gateway: String,
|
||||
#[serde(default)]
|
||||
dns: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -458,6 +871,15 @@ fn configure_tap_adapter(request: TapAddressRequest) -> Result<(), String> {
|
||||
&format!("address={address}"),
|
||||
"mask=255.255.255.0",
|
||||
]);
|
||||
let gateway = request.gateway.trim();
|
||||
if gateway.is_empty() {
|
||||
command.arg("gateway=none");
|
||||
} else {
|
||||
let gateway = gateway
|
||||
.parse::<Ipv4Addr>()
|
||||
.map_err(|_| format!("无效的网关:{gateway}"))?;
|
||||
command.arg(format!("gateway={gateway}"));
|
||||
}
|
||||
}
|
||||
let output = command
|
||||
.output()
|
||||
@@ -470,6 +892,35 @@ fn configure_tap_adapter(request: TapAddressRequest) -> Result<(), String> {
|
||||
};
|
||||
return Err(format!("TAP 网卡地址配置失败:{}", detail.trim()));
|
||||
}
|
||||
if !request.dhcp && !request.dns.trim().is_empty() {
|
||||
let dns = request
|
||||
.dns
|
||||
.trim()
|
||||
.parse::<Ipv4Addr>()
|
||||
.map_err(|_| format!("无效的 DNS:{}", request.dns.trim()))?;
|
||||
let dns_output = Command::new("netsh")
|
||||
.args([
|
||||
"interface",
|
||||
"ipv4",
|
||||
"set",
|
||||
"dnsservers",
|
||||
&interface,
|
||||
"source=static",
|
||||
&format!("address={dns}"),
|
||||
"register=primary",
|
||||
"validate=no",
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("无法配置 TAP DNS:{error}"))?;
|
||||
if !dns_output.status.success() {
|
||||
let detail = if dns_output.stderr.is_empty() {
|
||||
String::from_utf8_lossy(&dns_output.stdout)
|
||||
} else {
|
||||
String::from_utf8_lossy(&dns_output.stderr)
|
||||
};
|
||||
return Err(format!("TAP DNS 配置失败:{}", detail.trim()));
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -482,18 +933,10 @@ fn configure_tap_adapter(request: TapAddressRequest) -> Result<(), String> {
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_service(service: String, state: State<'_, AppState>) -> Result<ServiceStatus, String> {
|
||||
let mut processes = state
|
||||
.processes
|
||||
.lock()
|
||||
.map_err(|_| "process state is unavailable".to_string())?;
|
||||
if let Some(mut child) = processes.remove(&service) {
|
||||
child
|
||||
.kill()
|
||||
.map_err(|e| format!("unable to stop {}: {}", service, e))?;
|
||||
let _ = child.wait();
|
||||
return Ok(empty_status(&service, "stopped"));
|
||||
if service == "supernode" {
|
||||
stop_special_server(state.inner());
|
||||
}
|
||||
Ok(empty_status(&service, "not running"))
|
||||
Ok(stop_service_process(&service, state.inner()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -838,7 +1281,10 @@ fn export_log(contents: String) -> Result<String, String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{is_tap_windows_description, relative_binary_path, StartRequest};
|
||||
use super::{
|
||||
cidr_range, is_tap_windows_description, relative_binary_path, validate_supernode_request,
|
||||
StartRequest, SupernodeRequest,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn tap_description_filter_excludes_network_filter_interfaces() {
|
||||
@@ -884,12 +1330,42 @@ mod tests {
|
||||
"./n2n/supernode.exe"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supernode_cidr_range_keeps_network_and_broadcast() {
|
||||
assert_eq!(
|
||||
cidr_range("10.10.10.128/25").unwrap(),
|
||||
"10.10.10.128-10.10.10.128/25"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn special_pool_must_be_inside_base_pool_and_allowed_community() {
|
||||
let valid = SupernodeRequest {
|
||||
port: 7777,
|
||||
dhcp: true,
|
||||
dhcp_pool: "10.10.10.0/24".to_string(),
|
||||
allowed_communities: "team\nops".to_string(),
|
||||
special_enabled: true,
|
||||
special_port: 7788,
|
||||
special_community: "team".to_string(),
|
||||
special_pool: "10.10.10.128/25".to_string(),
|
||||
special_gateway: "10.10.10.1".to_string(),
|
||||
special_dns: "10.10.10.1".to_string(),
|
||||
};
|
||||
assert!(validate_supernode_request(&valid).is_ok());
|
||||
|
||||
let mut invalid = valid;
|
||||
invalid.special_community = "guest".to_string();
|
||||
assert!(validate_supernode_request(&invalid).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
let app = tauri::Builder::default()
|
||||
.manage(AppState {
|
||||
processes: Mutex::new(HashMap::new()),
|
||||
special_server: Mutex::new(None),
|
||||
})
|
||||
.setup(|app| {
|
||||
let connect = MenuItem::with_id(app, "tray-connect", "连接", true, None::<&str>)?;
|
||||
@@ -938,14 +1414,27 @@ pub fn run() {
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
stop_all_processes(window.state::<AppState>().inner());
|
||||
let _ = window.emit("tray-service-stopped", ());
|
||||
let _ = window.hide();
|
||||
if window.label() == "main" {
|
||||
stop_all_processes(window.state::<AppState>().inner());
|
||||
if let Some(users_window) =
|
||||
window.app_handle().get_webview_window("supernode-users")
|
||||
{
|
||||
let _ = users_window.destroy();
|
||||
}
|
||||
let _ = window.emit("tray-service-stopped", ());
|
||||
let _ = window.hide();
|
||||
} else if window.label() == "supernode-users" {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_service,
|
||||
start_supernode,
|
||||
stop_service,
|
||||
stop_special_service,
|
||||
fetch_special_config,
|
||||
fetch_special_users,
|
||||
service_status,
|
||||
management_query,
|
||||
run_ping,
|
||||
|
||||
Reference in New Issue
Block a user