diff --git a/vnt-core/build.rs b/vnt-core/build.rs index 9b36431..e4ca6b1 100644 --- a/vnt-core/build.rs +++ b/vnt-core/build.rs @@ -1,4 +1,4 @@ -fn main() { +fn main() -> Result<(), Box> { let mut config = prost_build::Config::new(); match protoc_bin_vendored::protoc_bin_path() { @@ -14,15 +14,14 @@ fn main() { config.protoc_arg("--experimental_allow_proto3_optional"); - config - .compile_protos( - &[ - "proto/control_message.proto", - "proto/rpc.proto", - "proto/client.proto", - "proto/fec.proto", - ], - &["proto"], - ) - .unwrap(); + config.compile_protos( + &[ + "proto/control_message.proto", + "proto/rpc.proto", + "proto/client.proto", + "proto/fec.proto", + ], + &["proto"], + )?; + Ok(()) } diff --git a/vnt-core/src/core/mod.rs b/vnt-core/src/core/mod.rs index c482ab8..ccce1a8 100644 --- a/vnt-core/src/core/mod.rs +++ b/vnt-core/src/core/mod.rs @@ -79,7 +79,7 @@ impl NetworkManager { log::info!("绑定出口网卡: {name}"); } let mtu = config.mtu.unwrap_or(DEFAULT_MTU); - let packet_crypto = PacketCrypto::new_from_str(config.password.as_deref()); + let packet_crypto = PacketCrypto::new_from_str(config.password.as_deref())?; let packet_compression = PacketCompression::new(config.compress); let (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel( app_state.clone(), @@ -163,7 +163,7 @@ impl NetworkManager { EnhancedTunInbound::Nat( internal_nat_inbound .clone() - .expect("internal_nat_inbound must be Some in no-device mode"), + .context("internal NAT is unavailable in no-device mode")?, ), None, ), diff --git a/vnt-core/src/crypto/chacha20_poly1305.rs b/vnt-core/src/crypto/chacha20_poly1305.rs index f1a6470..08a37bb 100644 --- a/vnt-core/src/crypto/chacha20_poly1305.rs +++ b/vnt-core/src/crypto/chacha20_poly1305.rs @@ -33,15 +33,16 @@ impl PacketCrypto { .map(|b| format!("{:02x}", b)) .collect::() } - pub fn new(key_bytes: [u8; 32]) -> Self { - let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes).unwrap(); + pub fn new(key_bytes: [u8; 32]) -> io::Result { + let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes) + .map_err(|_| io::Error::other("failed to initialize ChaCha20-Poly1305 key"))?; let key = LessSafeKey::new(unbound); - Self { + Ok(Self { key, seq: Arc::new(AtomicU32::new(rand::random())), - } + }) } - pub fn new_from_str(s: &str) -> Self { + pub fn new_from_str(s: &str) -> io::Result { let hash = ring::digest::digest(&ring::digest::SHA256, s.as_bytes()); let mut key_bytes = [0u8; 32]; key_bytes.copy_from_slice(hash.as_ref()); @@ -161,7 +162,7 @@ mod tests { #[test] fn test_encrypt_decrypt_in_place() { let key = [7u8; 32]; - let crypto = PacketCrypto::new(key); + let crypto = PacketCrypto::new(key).unwrap(); let payload_len = 20; let mut pkt = build_test_packet(payload_len); @@ -198,7 +199,7 @@ mod tests { #[test] fn test_nonce_unique_per_packet() { - let crypto = PacketCrypto::new([7u8; 32]); + let crypto = PacketCrypto::new([7u8; 32]).unwrap(); let mut pkt1 = build_test_packet(20); let mut pkt2 = build_test_packet(20); @@ -227,7 +228,7 @@ mod tests { #[test] fn test_clone_shares_seq_counter() { - let crypto = PacketCrypto::new([9u8; 32]); + let crypto = PacketCrypto::new([9u8; 32]).unwrap(); let cloned = crypto.clone(); let mut pkt1 = build_test_packet(8); @@ -243,9 +244,9 @@ mod tests { #[test] fn test_cross_version_compat() { let key = [7u8; 32]; - let crypto = PacketCrypto::new(key); + let crypto = PacketCrypto::new(key).unwrap(); // 用相同密钥的另一个实例模拟对端 - let peer = PacketCrypto::new(key); + let peer = PacketCrypto::new(key).unwrap(); // 模拟旧版本发包:seq 固定为 0,nonce 直接由头部计算 let mut pkt = build_test_packet(20); @@ -281,7 +282,7 @@ mod tests { /// 必须导致解密失败,而不是被静默接受。 #[test] fn test_tampered_flags_rejected() { - let crypto = PacketCrypto::new([7u8; 32]); + let crypto = PacketCrypto::new([7u8; 32]).unwrap(); let mut pkt = build_test_packet(20); crypto.encrypt_in_place(&mut pkt).expect("encrypt failed"); @@ -298,7 +299,7 @@ mod tests { /// AAD 覆盖 msg_type(byte0):中间人篡改消息类型必须导致解密失败。 #[test] fn test_tampered_msg_type_rejected() { - let crypto = PacketCrypto::new([7u8; 32]); + let crypto = PacketCrypto::new([7u8; 32]).unwrap(); let mut pkt = build_test_packet(20); crypto.encrypt_in_place(&mut pkt).expect("encrypt failed"); @@ -315,7 +316,7 @@ mod tests { /// 转发后 ttl 变化的包必须仍能正常解密。 #[test] fn test_ttl_change_still_decrypts() { - let crypto = PacketCrypto::new([7u8; 32]); + let crypto = PacketCrypto::new([7u8; 32]).unwrap(); let payload_len = 20; let mut pkt = build_test_packet(payload_len); diff --git a/vnt-core/src/crypto/mod.rs b/vnt-core/src/crypto/mod.rs index 6b9ae6c..ac3452f 100644 --- a/vnt-core/src/crypto/mod.rs +++ b/vnt-core/src/crypto/mod.rs @@ -16,12 +16,12 @@ impl PacketCrypto { chacha20_poly1305::PacketCrypto::key_sign(s) } - pub(crate) fn new_from_str(s: Option<&str>) -> Self { - Self { - crypto: s - .map(chacha20_poly1305::PacketCrypto::new_from_str) - .map(Arc::new), - } + pub(crate) fn new_from_str(s: Option<&str>) -> io::Result { + let crypto = s + .map(chacha20_poly1305::PacketCrypto::new_from_str) + .transpose()? + .map(Arc::new); + Ok(Self { crypto }) } pub(crate) fn encrypt_reserve(&self) -> usize { if self.crypto.is_some() { TAG_LEN } else { 0 } diff --git a/vnt-core/src/enhanced_tunnel/quic_over/boot.rs b/vnt-core/src/enhanced_tunnel/quic_over/boot.rs index 2d96c82..61d8502 100644 --- a/vnt-core/src/enhanced_tunnel/quic_over/boot.rs +++ b/vnt-core/src/enhanced_tunnel/quic_over/boot.rs @@ -145,7 +145,7 @@ async fn create_quic_endpoint( quinn::crypto::rustls::QuicClientConfig::try_from(client_config) .context("Failed to create QUIC client config")?, )); - client_config.transport_config(build_transport_config()); + client_config.transport_config(build_transport_config()?); let mut endpoint_config = EndpointConfig::default(); endpoint_config.max_udp_payload_size(1300)?; let mut endpoint = quinn::Endpoint::new_with_abstract_socket( @@ -159,14 +159,18 @@ async fn create_quic_endpoint( Ok((inbound, endpoint)) } -fn build_transport_config() -> Arc { +fn build_transport_config() -> anyhow::Result> { let mut transport = TransportConfig::default(); transport.congestion_controller_factory(Arc::new(BbrConfig::default())); transport.keep_alive_interval(Some(Duration::from_secs(5))); - transport.max_idle_timeout(Some(Duration::from_secs(10).try_into().unwrap())); + transport.max_idle_timeout(Some( + Duration::from_secs(10) + .try_into() + .context("invalid QUIC idle timeout")?, + )); - Arc::new(transport) + Ok(Arc::new(transport)) } async fn ip_stack_recv_task( diff --git a/vnt-core/src/nat/internal_nat/udp_nat.rs b/vnt-core/src/nat/internal_nat/udp_nat.rs index fb8c816..8b05698 100644 --- a/vnt-core/src/nat/internal_nat/udp_nat.rs +++ b/vnt-core/src/nat/internal_nat/udp_nat.rs @@ -85,9 +85,9 @@ async fn handle_outbound( } else { // 创建真实 UDP socket let bind_addr = if dst.is_ipv4() { - "0.0.0.0:0".parse().expect("valid IPv4 bind address") + SocketAddr::from(([0, 0, 0, 0], 0)) } else { - "[::]:0".parse().expect("valid IPv6 bind address") + SocketAddr::from(([0; 8], 0)) }; let interface = if dst.ip().is_loopback() { None @@ -223,9 +223,9 @@ where .next() .context("UDP NAT destination resolved to no address")?; let bind_addr = if destination.is_ipv4() { - "0.0.0.0:0".parse().expect("valid IPv4 bind address") + SocketAddr::from(([0, 0, 0, 0], 0)) } else { - "[::]:0".parse().expect("valid IPv6 bind address") + SocketAddr::from(([0; 8], 0)) }; let interface = if destination.ip().is_loopback() { None diff --git a/vnt-core/src/protocol/ip_packet_protocol.rs b/vnt-core/src/protocol/ip_packet_protocol.rs index 5e2d5b0..7277cd7 100644 --- a/vnt-core/src/protocol/ip_packet_protocol.rs +++ b/vnt-core/src/protocol/ip_packet_protocol.rs @@ -18,7 +18,7 @@ use crate::protocol::transmission::TransmissionBytes; use bytes::{Bytes, BytesMut}; use std::io; use zerocopy::byteorder::{NetworkEndian, U32}; -use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned}; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; #[derive(Debug, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)] #[repr(C)] @@ -167,11 +167,6 @@ impl> NetPacket { } Ok(NetPacket { buffer }) } - fn header(&self) -> Ref<&[u8], NetHeader> { - // Safe: NetHeader is Unaligned and length is validated in new() - let (header, _) = Ref::<&[u8], NetHeader>::from_prefix(self.buffer.as_ref()).unwrap(); - header - } pub fn buffer(&self) -> &[u8] { self.buffer.as_ref() } @@ -182,37 +177,40 @@ impl> NetPacket { &self.buffer } pub fn msg_type(&self) -> io::Result { - self.header().msg_type().try_into() + (self.buffer.as_ref()[0] & 0x7F).try_into() } pub fn max_ttl(&self) -> u8 { - self.header().max_ttl() + self.buffer.as_ref()[1] >> 4 } pub fn ttl(&self) -> u8 { - self.header().curr_ttl() + self.buffer.as_ref()[1] & 0x0F } pub fn seq(&self) -> u32 { - self.header().seq.get() + let buf = self.buffer.as_ref(); + u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]) } pub fn src_id(&self) -> u32 { - self.header().src_id.get() + let buf = self.buffer.as_ref(); + u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]) } pub fn dest_id(&self) -> u32 { - self.header().dest_id.get() + let buf = self.buffer.as_ref(); + u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]) } pub fn is_compressed(&self) -> bool { - (self.header().flags_byte & COMPRESSED) != 0 + (self.buffer.as_ref()[2] & COMPRESSED) != 0 } pub fn is_gateway(&self) -> bool { - (self.header().flags_byte & GATEWAY) != 0 + (self.buffer.as_ref()[2] & GATEWAY) != 0 } pub fn is_fec(&self) -> bool { - (self.header().flags_byte & FEC) != 0 + (self.buffer.as_ref()[2] & FEC) != 0 } pub fn is_ethernet(&self) -> bool { - (self.header().flags_byte & ETHERNET) != 0 + (self.buffer.as_ref()[2] & ETHERNET) != 0 } pub fn head(&self) -> &[u8] { &self.buffer.as_ref()[..HEAD_LENGTH] @@ -223,47 +221,54 @@ impl> NetPacket { } impl + AsMut<[u8]>> NetPacket { - fn header_mut(&mut self) -> Ref<&mut [u8], NetHeader> { - // Safe: NetHeader is Unaligned and length is validated in new() - let (header, _) = Ref::<&mut [u8], NetHeader>::from_prefix(self.buffer.as_mut()).unwrap(); - header - } - pub fn set_msg_type(&mut self, msg_type: MsgType) { - self.header_mut().set_msg_type(msg_type.into()); + self.buffer.as_mut()[0] = (u8::from(msg_type) & 0x7F) | 0x80; } pub fn decr_ttl(&mut self) { - self.header_mut().decr_ttl() + let ttl_byte = &mut self.buffer.as_mut()[1]; + let current = *ttl_byte & 0x0F; + if current != 0 { + *ttl_byte = (*ttl_byte & 0xF0) | (current - 1); + } } pub fn set_ttl(&mut self, ttl: u8) { - self.header_mut().set_ttl(ttl, ttl); + self.buffer.as_mut()[1] = (ttl << 4) | (ttl & 0x0F); } pub fn set_seq(&mut self, seq: u32) { - self.header_mut().seq.set(seq); + self.buffer.as_mut()[4..8].copy_from_slice(&seq.to_be_bytes()); } pub fn set_src_id(&mut self, id: u32) { - self.header_mut().src_id.set(id); + self.buffer.as_mut()[8..12].copy_from_slice(&id.to_be_bytes()); } pub fn set_dest_id(&mut self, id: u32) { - self.header_mut().dest_id.set(id); + self.buffer.as_mut()[12..16].copy_from_slice(&id.to_be_bytes()); + } + + fn set_flag(&mut self, mask: u8, value: bool) { + let flags = &mut self.buffer.as_mut()[2]; + if value { + *flags |= mask; + } else { + *flags &= !mask; + } } pub fn set_compressed_flag(&mut self, compressed: bool) { - self.header_mut().set_flag(COMPRESSED, compressed); + self.set_flag(COMPRESSED, compressed); } pub fn set_gateway_flag(&mut self, gateway: bool) { - self.header_mut().set_flag(GATEWAY, gateway); + self.set_flag(GATEWAY, gateway); } pub fn set_fec_flag(&mut self, fec: bool) { - self.header_mut().set_flag(FEC, fec); + self.set_flag(FEC, fec); } pub fn set_ethernet_flag(&mut self, ethernet: bool) { - self.header_mut().set_flag(ETHERNET, ethernet); + self.set_flag(ETHERNET, ethernet); } pub fn set_payload(&mut self, data: &[u8]) -> io::Result<()> { diff --git a/vnt-core/src/tun/general.rs b/vnt-core/src/tun/general.rs index 88571c9..95e5299 100644 --- a/vnt-core/src/tun/general.rs +++ b/vnt-core/src/tun/general.rs @@ -99,12 +99,17 @@ impl DeviceIOManager { // 保证失败时调用方状态完整、可以重试 let device_mode = device_config.device_mode; let device = Arc::new(create_device(device_config)?); - let receiver = receiver.take().unwrap(); - let enhanced_outbound = enhanced_outbound.take().unwrap(); + let Some(receiver_value) = receiver.take() else { + bail!("device task already started"); + }; + let Some(enhanced_outbound) = enhanced_outbound.take() else { + *receiver = Some(receiver_value); + bail!("device task already started"); + }; let task = create( &self.task_group, device, - receiver.receiver, + receiver_value.receiver, enhanced_outbound, device_mode, ); diff --git a/vnt-core/src/tunnel_core/p2p/transport/nat_test.rs b/vnt-core/src/tunnel_core/p2p/transport/nat_test.rs index 2d8a014..f8c79a5 100644 --- a/vnt-core/src/tunnel_core/p2p/transport/nat_test.rs +++ b/vnt-core/src/tunnel_core/p2p/transport/nat_test.rs @@ -357,7 +357,9 @@ pub(crate) async fn query_tcp_public_addr_loop( let addrs: Vec = active_connections.keys().cloned().collect(); for addr in addrs { - let (tcp_stream, _) = active_connections.get_mut(&addr).unwrap(); + let Some((tcp_stream, _)) = active_connections.get_mut(&addr) else { + continue; + }; let mut buf = [0u8; 1024]; match tcp_stream.try_read(&mut buf) { diff --git a/vnt-core/src/tunnel_core/p2p/transport/task.rs b/vnt-core/src/tunnel_core/p2p/transport/task.rs index 17e38a3..9d78591 100644 --- a/vnt-core/src/tunnel_core/p2p/transport/task.rs +++ b/vnt-core/src/tunnel_core/p2p/transport/task.rs @@ -149,8 +149,11 @@ pub async fn ping_all( ping.set_ttl(1); ping.set_src_id(src.into()); ping.set_dest_id(id.into()); - ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes()) - .unwrap(); + if let Err(error) = ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes()) + { + log::warn!("failed to build route probe: {error}"); + continue; + } let route_key = route.route_key(); if socket_manager.send_to(ping, &route_key).await.is_ok() { packet_loss_stats.record_sent(id, route_key); diff --git a/vnt-core/src/tunnel_core/server/outbound.rs b/vnt-core/src/tunnel_core/server/outbound.rs index 51b4669..7fb284f 100644 --- a/vnt-core/src/tunnel_core/server/outbound.rs +++ b/vnt-core/src/tunnel_core/server/outbound.rs @@ -151,7 +151,10 @@ impl ServerOutbound { // 只有一个服务器,直接发送 if map.len() == 1 { - let (server_id, (ips, _)) = map.iter().next().expect("map has exactly one element"); + let (server_id, (ips, _)) = map + .iter() + .next() + .context("connected server map unexpectedly became empty")?; if ips.is_empty() { return Ok(()); } diff --git a/vnt-core/src/utils/dns_query.rs b/vnt-core/src/utils/dns_query.rs index 52b7f08..d3ed925 100644 --- a/vnt-core/src/utils/dns_query.rs +++ b/vnt-core/src/utils/dns_query.rs @@ -228,11 +228,9 @@ fn bind_udp( default_interface: &Option, ) -> io::Result { let addr: SocketAddr = if name_server.is_ipv4() { - "0.0.0.0:0" - .parse() - .expect("valid IPv4 socket address literal") + SocketAddr::from(([0, 0, 0, 0], 0)) } else { - "[::]:0".parse().expect("valid IPv6 socket address literal") + SocketAddr::from(([0; 8], 0)) }; let socket = rust_p2p_core::socket::bind_udp(addr, default_interface.as_ref())?; UdpSocket::from_std(socket.into()) diff --git a/vnt-desktop/src-tauri/src/lib.rs b/vnt-desktop/src-tauri/src/lib.rs index 3d5b479..dcf5c27 100644 --- a/vnt-desktop/src-tauri/src/lib.rs +++ b/vnt-desktop/src-tauri/src/lib.rs @@ -235,7 +235,7 @@ fn toggle_main_window(app: &AppHandle) { } } -pub fn run() { +pub fn run() -> Result<(), tauri::Error> { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ api_request, @@ -320,5 +320,4 @@ pub fn run() { } }) .run(tauri::generate_context!()) - .expect("error while running VNT Desktop"); } diff --git a/vnt-desktop/src-tauri/src/main.rs b/vnt-desktop/src-tauri/src/main.rs index 39949ae..6565be4 100644 --- a/vnt-desktop/src-tauri/src/main.rs +++ b/vnt-desktop/src-tauri/src/main.rs @@ -1,3 +1,3 @@ -fn main() { - vnt_desktop_lib::run(); +fn main() -> Result<(), tauri::Error> { + vnt_desktop_lib::run() } diff --git a/vnt-ipc/build.rs b/vnt-ipc/build.rs index c3ccfd8..82120db 100644 --- a/vnt-ipc/build.rs +++ b/vnt-ipc/build.rs @@ -1,4 +1,4 @@ -fn main() { +fn main() -> Result<(), Box> { let mut config = prost_build::Config::new(); match protoc_bin_vendored::protoc_bin_path() { @@ -13,7 +13,6 @@ fn main() { } config.protoc_arg("--experimental_allow_proto3_optional"); - config - .compile_protos(&["proto/local_ipc.proto"], &["proto"]) - .unwrap(); + config.compile_protos(&["proto/local_ipc.proto"], &["proto"])?; + Ok(()) } diff --git a/vnt-ipc/src/client.rs b/vnt-ipc/src/client.rs index 9c46aa9..5b06238 100644 --- a/vnt-ipc/src/client.rs +++ b/vnt-ipc/src/client.rs @@ -285,5 +285,5 @@ pub fn ts_to_string(ts_secs: i64) -> String { }; let dt_local = dt.to_offset(local_offset); let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); - dt_local.format(&format).unwrap() + dt_local.format(&format).unwrap_or_default() } diff --git a/vnt-web/build.rs b/vnt-web/build.rs index e3b1ea5..ebc43b5 100644 --- a/vnt-web/build.rs +++ b/vnt-web/build.rs @@ -5,16 +5,20 @@ //! - 找不到 pnpm 时:已有产物则告警并沿用;没有产物则报错并给出指引 //! - 设置环境变量 VNT_WEB_SKIP_UI_BUILD=1 可完全跳过前端构建 +use std::error::Error; +use std::io; use std::path::Path; use std::process::Command; use std::time::SystemTime; -fn main() { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"); +fn main() -> Result<(), Box> { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?; let manifest_dir = Path::new(&manifest_dir); let ui_dir = manifest_dir.join("ui"); let static_dir = manifest_dir.join("static"); - let workspace_root = manifest_dir.parent().expect("workspace root"); + let workspace_root = manifest_dir + .parent() + .ok_or_else(|| io::Error::other("vnt-web manifest directory has no parent"))?; // UI 源码变化时重新运行本脚本 println!("cargo:rerun-if-changed={}", ui_dir.join("src").display()); @@ -38,12 +42,12 @@ fn main() { println!("cargo:rerun-if-env-changed=VNT_WEB_SKIP_UI_BUILD"); if std::env::var("VNT_WEB_SKIP_UI_BUILD").is_ok() { - ensure_static_placeholder(&static_dir); - return; + ensure_static_placeholder(&static_dir)?; + return Ok(()); } if static_is_fresh(&ui_dir, &static_dir) { - return; + return Ok(()); } let Some(pnpm) = find_pnpm() else { @@ -51,20 +55,22 @@ fn main() { println!( "cargo:warning=未找到 pnpm,沿用 vnt-web/static 中已有的前端产物(可能不是最新)" ); - return; + return Ok(()); } - panic!( + return Err(io::Error::other( "未找到 pnpm 且 vnt-web/static 没有前端产物。\n\ 请安装 Node.js 与 pnpm 后重新构建(cargo 会自动完成前端构建),\n\ - 或从发布包中获取 static 目录放入 vnt-web/。" - ); + 或从发布包中获取 static 目录放入 vnt-web/。", + ) + .into()); }; if !ui_dir.join("node_modules").is_dir() { // ui 依赖使用 workspace catalog,必须在仓库根目录安装 - run_or_panic(pnpm, &["install", "--frozen-lockfile"], workspace_root); + run_command(pnpm, &["install", "--frozen-lockfile"], workspace_root)?; } - run_or_panic(pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root); + run_command(pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root)?; + Ok(()) } /// pnpm 命令名(Windows 上是 pnpm.cmd,由 cmd.exe 执行) @@ -80,7 +86,7 @@ fn find_pnpm() -> Option<&'static str> { .find(|cmd| Command::new(cmd).arg("--version").output().is_ok()) } -fn run_or_panic(program: &str, args: &[&str], dir: &Path) { +fn run_command(program: &str, args: &[&str], dir: &Path) -> io::Result<()> { println!( "cargo:warning=执行前端构建: {} {} ({})", program, @@ -91,15 +97,21 @@ fn run_or_panic(program: &str, args: &[&str], dir: &Path) { .args(args) .current_dir(dir) .status() - .unwrap_or_else(|e| panic!("执行 {} 失败: {}", program, e)); + .map_err(|error| { + io::Error::new( + error.kind(), + format!("执行 {program} 失败(目录 {}):{error}", dir.display()), + ) + })?; if !status.success() { - panic!( + return Err(io::Error::other(format!( "前端构建失败: {} {} (exit: {:?})", program, args.join(" "), status.code() - ); + ))); } + Ok(()) } /// static 产物是否比 UI 源码新 @@ -128,16 +140,16 @@ fn newest_mtime(dir: &Path) -> Option { } /// 跳过构建时保证 static/ 存在,使 rust_embed 可以编译 -fn ensure_static_placeholder(static_dir: &Path) { +fn ensure_static_placeholder(static_dir: &Path) -> io::Result<()> { if static_dir.join("index.html").is_file() { - return; + return Ok(()); } println!("cargo:warning=VNT_WEB_SKIP_UI_BUILD 已设置且 static 为空,写入占位页面"); - std::fs::create_dir_all(static_dir).expect("创建 static 目录失败"); + std::fs::create_dir_all(static_dir)?; std::fs::write( static_dir.join("index.html"), "

VNT Web UI 未构建。请安装 pnpm 后重新执行 cargo build,\ 或取消 VNT_WEB_SKIP_UI_BUILD。

", - ) - .expect("写入占位页面失败"); + )?; + Ok(()) } diff --git a/vnt-web/src/service_http.rs b/vnt-web/src/service_http.rs index cdd9919..eca6deb 100644 --- a/vnt-web/src/service_http.rs +++ b/vnt-web/src/service_http.rs @@ -21,7 +21,7 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Instant, SystemTime, UNIX_EPOCH}; -use time::{OffsetDateTime, format_description}; +use time::{OffsetDateTime, macros::format_description}; use tokio::fs; use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; @@ -205,7 +205,7 @@ impl HttpAppState { fn timestamp() -> String { let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); - let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + let format = format_description!("[hour]:[minute]:[second]"); now.format(&format) .unwrap_or_else(|_| "00:00:00".to_string()) } @@ -649,9 +649,10 @@ pub async fn run_http_server( let handle = service .start_http(addr, token, cancellation.clone()) .await?; - shutdown_signal().await; + let shutdown_result = shutdown_signal().await; cancellation.cancel(); handle.await??; + shutdown_result?; Ok(()) } @@ -751,7 +752,8 @@ fn build_headers_for_path(path: &str) -> HeaderMap { }; headers.insert( header::CONTENT_TYPE, - HeaderValue::from_str(mime.as_ref()).unwrap(), + HeaderValue::from_str(mime.as_ref()) + .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")), ); if is_gz { @@ -1309,7 +1311,7 @@ async fn save_config(Json(req): Json) -> Json> { .unwrap_or_else(|| { let now = SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_millis(); format!("{}.toml", now) }); @@ -1455,28 +1457,29 @@ fn convert_config(cfg: StartConfig) -> anyhow::Result { }) } -async fn shutdown_signal() { +async fn shutdown_signal() -> anyhow::Result<()> { let ctrl_c = async { tokio::signal::ctrl_c() .await - .expect("failed to install Ctrl+C handler"); + .context("failed to install Ctrl+C handler") }; #[cfg(unix)] let terminate = async { - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; + let mut signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .context("failed to install terminate signal handler")?; + signal.recv().await; + Ok::<(), anyhow::Error>(()) }; #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); + let terminate = std::future::pending::>(); tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, + result = ctrl_c => result?, + result = terminate => result?, } + Ok(()) } async fn get_peers(