mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
feat(rustdesk): 完整实现RustDesk协议和P2P连接
重大变更: - 从prost切换到protobuf 3.4实现完整的RustDesk协议栈 - 新增P2P打洞模块(punch.rs)支持直连和中继回退 - 重构加密系统:临时Curve25519密钥对+Ed25519签名 - 完善HID适配器:支持CapsLock状态同步和修饰键映射 - 添加音频流支持:Opus编码+音频帧适配器 - 优化视频流:改进帧适配器和编码器协商 - 移除pacer.rs简化视频管道 扩展系统: - 在设置向导中添加扩展步骤(ttyd/rustdesk切换) - 扩展可用性检测和自动启动 - 新增WebConfig handler用于Web服务器配置 前端改进: - SetupView增加第4步扩展配置 - 音频设备列表和配置界面 - 新增多语言支持(en-US/zh-CN) - TypeScript类型生成更新 文档: - 更新系统架构文档 - 完善config/hid/rustdesk/video/webrtc模块文档
This commit is contained in:
@@ -156,11 +156,25 @@ pub async fn apply_hid_config(
|
||||
old_config: &HidConfig,
|
||||
new_config: &HidConfig,
|
||||
) -> Result<()> {
|
||||
// 检查是否需要重载
|
||||
// 检查 OTG 描述符是否变更
|
||||
let descriptor_changed = old_config.otg_descriptor != new_config.otg_descriptor;
|
||||
|
||||
// 如果描述符变更且当前使用 OTG 后端,需要重建 Gadget
|
||||
if descriptor_changed && new_config.backend == HidBackend::Otg {
|
||||
tracing::info!("OTG descriptor changed, updating gadget...");
|
||||
if let Err(e) = state.otg_service.update_descriptor(&new_config.otg_descriptor).await {
|
||||
tracing::error!("Failed to update OTG descriptor: {}", e);
|
||||
return Err(AppError::Config(format!("OTG descriptor update failed: {}", e)));
|
||||
}
|
||||
tracing::info!("OTG descriptor updated successfully");
|
||||
}
|
||||
|
||||
// 检查是否需要重载 HID 后端
|
||||
if old_config.backend == new_config.backend
|
||||
&& old_config.ch9329_port == new_config.ch9329_port
|
||||
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
|
||||
&& old_config.otg_udc == new_config.otg_udc
|
||||
&& !descriptor_changed
|
||||
{
|
||||
tracing::info!("HID config unchanged, skipping reload");
|
||||
return Ok(());
|
||||
@@ -390,6 +404,8 @@ pub async fn apply_rustdesk_config(
|
||||
|| old_config.device_id != new_config.device_id
|
||||
|| old_config.device_password != new_config.device_password;
|
||||
|
||||
let mut credentials_to_save = None;
|
||||
|
||||
if rustdesk_guard.is_none() {
|
||||
// Create new service
|
||||
tracing::info!("Initializing RustDesk service...");
|
||||
@@ -403,6 +419,8 @@ pub async fn apply_rustdesk_config(
|
||||
tracing::error!("Failed to start RustDesk service: {}", e);
|
||||
} else {
|
||||
tracing::info!("RustDesk service started with ID: {}", new_config.device_id);
|
||||
// Save generated keypair and UUID to config
|
||||
credentials_to_save = service.save_credentials();
|
||||
}
|
||||
*rustdesk_guard = Some(std::sync::Arc::new(service));
|
||||
} else if need_restart {
|
||||
@@ -412,9 +430,32 @@ pub async fn apply_rustdesk_config(
|
||||
tracing::error!("Failed to restart RustDesk service: {}", e);
|
||||
} else {
|
||||
tracing::info!("RustDesk service restarted with ID: {}", new_config.device_id);
|
||||
// Save generated keypair and UUID to config
|
||||
credentials_to_save = service.save_credentials();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save credentials to persistent config store (outside the lock)
|
||||
drop(rustdesk_guard);
|
||||
if let Some(updated_config) = credentials_to_save {
|
||||
tracing::info!("Saving RustDesk credentials to config store...");
|
||||
if let Err(e) = state
|
||||
.config
|
||||
.update(|cfg| {
|
||||
cfg.rustdesk.public_key = updated_config.public_key.clone();
|
||||
cfg.rustdesk.private_key = updated_config.private_key.clone();
|
||||
cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone();
|
||||
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone();
|
||||
cfg.rustdesk.uuid = updated_config.uuid.clone();
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to save RustDesk credentials: {}", e);
|
||||
} else {
|
||||
tracing::info!("RustDesk credentials saved successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -16,16 +16,17 @@
|
||||
//! - GET /api/config/rustdesk - 获取 RustDesk 配置
|
||||
//! - PATCH /api/config/rustdesk - 更新 RustDesk 配置
|
||||
|
||||
mod apply;
|
||||
pub(crate) mod apply;
|
||||
mod types;
|
||||
|
||||
mod video;
|
||||
pub(crate) mod video;
|
||||
mod stream;
|
||||
mod hid;
|
||||
mod msd;
|
||||
mod atx;
|
||||
mod audio;
|
||||
mod rustdesk;
|
||||
mod web;
|
||||
|
||||
// 导出 handler 函数
|
||||
pub use video::{get_video_config, update_video_config};
|
||||
@@ -38,6 +39,7 @@ pub use rustdesk::{
|
||||
get_rustdesk_config, get_rustdesk_status, update_rustdesk_config,
|
||||
regenerate_device_id, regenerate_device_password, get_device_password,
|
||||
};
|
||||
pub use web::{get_web_config, update_web_config};
|
||||
|
||||
// 保留全局配置查询(向后兼容)
|
||||
use axum::{extract::State, Json};
|
||||
|
||||
@@ -21,6 +21,8 @@ pub struct RustDeskConfigResponse {
|
||||
pub has_password: bool,
|
||||
/// 是否已设置密钥对
|
||||
pub has_keypair: bool,
|
||||
/// 是否已设置 relay key
|
||||
pub has_relay_key: bool,
|
||||
/// 是否使用公共服务器(用户留空时)
|
||||
pub using_public_server: bool,
|
||||
}
|
||||
@@ -34,6 +36,7 @@ impl From<&RustDeskConfig> for RustDeskConfigResponse {
|
||||
device_id: config.device_id.clone(),
|
||||
has_password: !config.device_password.is_empty(),
|
||||
has_keypair: config.public_key.is_some() && config.private_key.is_some(),
|
||||
has_relay_key: config.relay_key.is_some(),
|
||||
using_public_server: config.is_using_public_server(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +159,60 @@ impl StreamConfigUpdate {
|
||||
}
|
||||
|
||||
// ===== HID Config =====
|
||||
|
||||
/// OTG USB device descriptor configuration update
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OtgDescriptorConfigUpdate {
|
||||
pub vendor_id: Option<u16>,
|
||||
pub product_id: Option<u16>,
|
||||
pub manufacturer: Option<String>,
|
||||
pub product: Option<String>,
|
||||
pub serial_number: Option<String>,
|
||||
}
|
||||
|
||||
impl OtgDescriptorConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
// Validate manufacturer string length
|
||||
if let Some(ref s) = self.manufacturer {
|
||||
if s.len() > 126 {
|
||||
return Err(AppError::BadRequest("Manufacturer string too long (max 126 chars)".into()));
|
||||
}
|
||||
}
|
||||
// Validate product string length
|
||||
if let Some(ref s) = self.product {
|
||||
if s.len() > 126 {
|
||||
return Err(AppError::BadRequest("Product string too long (max 126 chars)".into()));
|
||||
}
|
||||
}
|
||||
// Validate serial number string length
|
||||
if let Some(ref s) = self.serial_number {
|
||||
if s.len() > 126 {
|
||||
return Err(AppError::BadRequest("Serial number string too long (max 126 chars)".into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut crate::config::OtgDescriptorConfig) {
|
||||
if let Some(v) = self.vendor_id {
|
||||
config.vendor_id = v;
|
||||
}
|
||||
if let Some(v) = self.product_id {
|
||||
config.product_id = v;
|
||||
}
|
||||
if let Some(ref v) = self.manufacturer {
|
||||
config.manufacturer = v.clone();
|
||||
}
|
||||
if let Some(ref v) = self.product {
|
||||
config.product = v.clone();
|
||||
}
|
||||
if let Some(ref v) = self.serial_number {
|
||||
config.serial_number = Some(v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct HidConfigUpdate {
|
||||
@@ -166,6 +220,7 @@ pub struct HidConfigUpdate {
|
||||
pub ch9329_port: Option<String>,
|
||||
pub ch9329_baudrate: Option<u32>,
|
||||
pub otg_udc: Option<String>,
|
||||
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
|
||||
pub mouse_absolute: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -179,6 +234,9 @@ impl HidConfigUpdate {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(ref desc) = self.otg_descriptor {
|
||||
desc.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -195,6 +253,9 @@ impl HidConfigUpdate {
|
||||
if let Some(ref udc) = self.otg_udc {
|
||||
config.otg_udc = Some(udc.clone());
|
||||
}
|
||||
if let Some(ref desc) = self.otg_descriptor {
|
||||
desc.apply_to(&mut config.otg_descriptor);
|
||||
}
|
||||
if let Some(absolute) = self.mouse_absolute {
|
||||
config.mouse_absolute = absolute;
|
||||
}
|
||||
@@ -389,6 +450,7 @@ pub struct RustDeskConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub rendezvous_server: Option<String>,
|
||||
pub relay_server: Option<String>,
|
||||
pub relay_key: Option<String>,
|
||||
pub device_password: Option<String>,
|
||||
}
|
||||
|
||||
@@ -431,6 +493,9 @@ impl RustDeskConfigUpdate {
|
||||
if let Some(ref server) = self.relay_server {
|
||||
config.relay_server = if server.is_empty() { None } else { Some(server.clone()) };
|
||||
}
|
||||
if let Some(ref key) = self.relay_key {
|
||||
config.relay_key = if key.is_empty() { None } else { Some(key.clone()) };
|
||||
}
|
||||
if let Some(ref password) = self.device_password {
|
||||
if !password.is_empty() {
|
||||
config.device_password = password.clone();
|
||||
@@ -438,3 +503,49 @@ impl RustDeskConfigUpdate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Web Config =====
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WebConfigUpdate {
|
||||
pub http_port: Option<u16>,
|
||||
pub https_port: Option<u16>,
|
||||
pub bind_address: Option<String>,
|
||||
pub https_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl WebConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
if let Some(port) = self.http_port {
|
||||
if port == 0 {
|
||||
return Err(AppError::BadRequest("HTTP port cannot be 0".into()));
|
||||
}
|
||||
}
|
||||
if let Some(port) = self.https_port {
|
||||
if port == 0 {
|
||||
return Err(AppError::BadRequest("HTTPS port cannot be 0".into()));
|
||||
}
|
||||
}
|
||||
if let Some(ref addr) = self.bind_address {
|
||||
if addr.parse::<std::net::IpAddr>().is_err() {
|
||||
return Err(AppError::BadRequest("Invalid bind address".into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut crate::config::WebConfig) {
|
||||
if let Some(port) = self.http_port {
|
||||
config.http_port = port;
|
||||
}
|
||||
if let Some(port) = self.https_port {
|
||||
config.https_port = port;
|
||||
}
|
||||
if let Some(ref addr) = self.bind_address {
|
||||
config.bind_address = addr.clone();
|
||||
}
|
||||
if let Some(enabled) = self.https_enabled {
|
||||
config.https_enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
src/web/handlers/config/web.rs
Normal file
32
src/web/handlers/config/web.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Web 服务器配置 Handler
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::WebConfig;
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::types::WebConfigUpdate;
|
||||
|
||||
/// 获取 Web 配置
|
||||
pub async fn get_web_config(State(state): State<Arc<AppState>>) -> Json<WebConfig> {
|
||||
Json(state.config.get().web.clone())
|
||||
}
|
||||
|
||||
/// 更新 Web 配置
|
||||
pub async fn update_web_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<WebConfigUpdate>,
|
||||
) -> Result<Json<WebConfig>> {
|
||||
req.validate()?;
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.web);
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(Json(state.config.get().web.clone()))
|
||||
}
|
||||
@@ -185,13 +185,26 @@ fn get_cpu_model() -> String {
|
||||
std::fs::read_to_string("/proc/cpuinfo")
|
||||
.ok()
|
||||
.and_then(|content| {
|
||||
content
|
||||
// Try to get model name
|
||||
let model = content
|
||||
.lines()
|
||||
.find(|line| line.starts_with("model name") || line.starts_with("Model"))
|
||||
.and_then(|line| line.split(':').nth(1))
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
if model.is_some() {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Fallback: show arch and core count
|
||||
let cores = content
|
||||
.lines()
|
||||
.filter(|line| line.starts_with("processor"))
|
||||
.count();
|
||||
Some(format!("{} {}C", std::env::consts::ARCH, cores))
|
||||
})
|
||||
.unwrap_or_else(|| "Unknown CPU".to_string())
|
||||
.unwrap_or_else(|| format!("{}", std::env::consts::ARCH))
|
||||
}
|
||||
|
||||
/// CPU usage state for calculating usage between samples
|
||||
@@ -482,11 +495,16 @@ pub struct SetupRequest {
|
||||
pub video_width: Option<u32>,
|
||||
pub video_height: Option<u32>,
|
||||
pub video_fps: Option<u32>,
|
||||
// Audio settings
|
||||
pub audio_device: Option<String>,
|
||||
// HID settings
|
||||
pub hid_backend: Option<String>,
|
||||
pub hid_ch9329_port: Option<String>,
|
||||
pub hid_ch9329_baudrate: Option<u32>,
|
||||
pub hid_otg_udc: Option<String>,
|
||||
// Extension settings
|
||||
pub ttyd_enabled: Option<bool>,
|
||||
pub rustdesk_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn setup_init(
|
||||
@@ -541,6 +559,12 @@ pub async fn setup_init(
|
||||
config.video.fps = fps;
|
||||
}
|
||||
|
||||
// Audio settings
|
||||
if let Some(device) = req.audio_device.clone() {
|
||||
config.audio.device = device;
|
||||
config.audio.enabled = true;
|
||||
}
|
||||
|
||||
// HID settings
|
||||
if let Some(backend) = req.hid_backend.clone() {
|
||||
config.hid.backend = match backend.as_str() {
|
||||
@@ -558,12 +582,26 @@ pub async fn setup_init(
|
||||
if let Some(udc) = req.hid_otg_udc.clone() {
|
||||
config.hid.otg_udc = Some(udc);
|
||||
}
|
||||
|
||||
// Extension settings
|
||||
if let Some(enabled) = req.ttyd_enabled {
|
||||
config.extensions.ttyd.enabled = enabled;
|
||||
}
|
||||
if let Some(enabled) = req.rustdesk_enabled {
|
||||
config.rustdesk.enabled = enabled;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Get updated config for HID reload
|
||||
let new_config = state.config.get();
|
||||
|
||||
tracing::info!(
|
||||
"Extension config after save: ttyd.enabled={}, rustdesk.enabled={}",
|
||||
new_config.extensions.ttyd.enabled,
|
||||
new_config.rustdesk.enabled
|
||||
);
|
||||
|
||||
// Initialize HID backend with new config
|
||||
let new_hid_backend = match new_config.hid.backend {
|
||||
crate::config::HidBackend::Otg => crate::hid::HidBackendType::Otg,
|
||||
@@ -582,6 +620,34 @@ pub async fn setup_init(
|
||||
tracing::info!("HID backend initialized: {:?}", new_config.hid.backend);
|
||||
}
|
||||
|
||||
// Start extensions if enabled
|
||||
if new_config.extensions.ttyd.enabled {
|
||||
if let Err(e) = state
|
||||
.extensions
|
||||
.start(
|
||||
crate::extensions::ExtensionId::Ttyd,
|
||||
&new_config.extensions,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to start ttyd during setup: {}", e);
|
||||
} else {
|
||||
tracing::info!("ttyd started during setup");
|
||||
}
|
||||
}
|
||||
|
||||
// Start RustDesk if enabled
|
||||
if new_config.rustdesk.enabled {
|
||||
let empty_config = crate::rustdesk::config::RustDeskConfig::default();
|
||||
if let Err(e) =
|
||||
config::apply::apply_rustdesk_config(&state, &empty_config, &new_config.rustdesk).await
|
||||
{
|
||||
tracing::warn!("Failed to start RustDesk during setup: {}", e);
|
||||
} else {
|
||||
tracing::info!("RustDesk started during setup");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("System initialized successfully with admin user: {}", req.username);
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
@@ -908,6 +974,13 @@ pub struct DeviceList {
|
||||
pub serial: Vec<SerialDevice>,
|
||||
pub audio: Vec<AudioDevice>,
|
||||
pub udc: Vec<UdcDevice>,
|
||||
pub extensions: ExtensionsAvailability,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ExtensionsAvailability {
|
||||
pub ttyd_available: bool,
|
||||
pub rustdesk_available: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -916,6 +989,7 @@ pub struct VideoDevice {
|
||||
pub name: String,
|
||||
pub driver: String,
|
||||
pub formats: Vec<VideoFormat>,
|
||||
pub usb_bus: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -942,6 +1016,8 @@ pub struct SerialDevice {
|
||||
pub struct AudioDevice {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub is_hdmi: bool,
|
||||
pub usb_bus: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -949,32 +1025,62 @@ pub struct UdcDevice {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Extract USB bus port from V4L2 bus_info string
|
||||
/// Examples:
|
||||
/// - "usb-0000:00:14.0-1" -> Some("1")
|
||||
/// - "usb-xhci-hcd.0-1.2" -> Some("1.2")
|
||||
/// - "usb-0000:00:14.0-1.3.2" -> Some("1.3.2")
|
||||
/// - "platform:..." -> None
|
||||
fn extract_usb_bus_from_bus_info(bus_info: &str) -> Option<String> {
|
||||
if !bus_info.starts_with("usb-") {
|
||||
return None;
|
||||
}
|
||||
// Find the last '-' which separates the USB port
|
||||
// e.g., "usb-0000:00:14.0-1" -> "1"
|
||||
// e.g., "usb-xhci-hcd.0-1.2" -> "1.2"
|
||||
let parts: Vec<&str> = bus_info.rsplitn(2, '-').collect();
|
||||
if parts.len() == 2 {
|
||||
let port = parts[0];
|
||||
// Verify it looks like a USB port (starts with digit)
|
||||
if port.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
|
||||
return Some(port.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList> {
|
||||
// Detect video devices
|
||||
let video_devices = match state.stream_manager.list_devices().await {
|
||||
Ok(devices) => devices
|
||||
.into_iter()
|
||||
.map(|d| VideoDevice {
|
||||
path: d.path.to_string_lossy().to_string(),
|
||||
name: d.name,
|
||||
driver: d.driver,
|
||||
formats: d
|
||||
.formats
|
||||
.iter()
|
||||
.map(|f| VideoFormat {
|
||||
format: format!("{}", f.format),
|
||||
description: f.description.clone(),
|
||||
resolutions: f
|
||||
.resolutions
|
||||
.iter()
|
||||
.map(|r| VideoResolution {
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
fps: r.fps.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
.map(|d| {
|
||||
// Extract USB bus from bus_info (e.g., "usb-0000:00:14.0-1" -> "1")
|
||||
// or "usb-xhci-hcd.0-1.2" -> "1.2"
|
||||
let usb_bus = extract_usb_bus_from_bus_info(&d.bus_info);
|
||||
VideoDevice {
|
||||
path: d.path.to_string_lossy().to_string(),
|
||||
name: d.name,
|
||||
driver: d.driver,
|
||||
formats: d
|
||||
.formats
|
||||
.iter()
|
||||
.map(|f| VideoFormat {
|
||||
format: format!("{}", f.format),
|
||||
description: f.description.clone(),
|
||||
resolutions: f
|
||||
.resolutions
|
||||
.iter()
|
||||
.map(|r| VideoResolution {
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
fps: r.fps.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
usb_bus,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
@@ -1024,16 +1130,25 @@ pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList
|
||||
.map(|d| AudioDevice {
|
||||
name: d.name,
|
||||
description: d.description,
|
||||
is_hdmi: d.is_hdmi,
|
||||
usb_bus: d.usb_bus,
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
// Check extension availability
|
||||
let ttyd_available = state.extensions.check_available(crate::extensions::ExtensionId::Ttyd);
|
||||
|
||||
Json(DeviceList {
|
||||
video: video_devices,
|
||||
serial: serial_devices,
|
||||
audio: audio_devices,
|
||||
udc: udc_devices,
|
||||
extensions: ExtensionsAvailability {
|
||||
ttyd_available,
|
||||
rustdesk_available: true, // RustDesk is built-in
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2574,3 +2689,53 @@ pub async fn change_user_password(
|
||||
message: Some("Password changed successfully".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// System Control
|
||||
// ============================================================================
|
||||
|
||||
/// Restart the application
|
||||
pub async fn system_restart(State(state): State<Arc<AppState>>) -> Json<LoginResponse> {
|
||||
info!("System restart requested via API");
|
||||
|
||||
// Send shutdown signal
|
||||
let _ = state.shutdown_tx.send(());
|
||||
|
||||
// Spawn restart task in background
|
||||
tokio::spawn(async {
|
||||
// Wait for resources to be released (OTG, video, etc.)
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Get current executable and args
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get current exe: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
|
||||
info!("Restarting: {:?} {:?}", exe, args);
|
||||
|
||||
// Use exec to replace current process (Unix)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = std::process::Command::new(&exe).args(&args).exec();
|
||||
tracing::error!("Failed to restart: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = std::process::Command::new(&exe).args(&args).spawn();
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
Json(LoginResponse {
|
||||
success: true,
|
||||
message: Some("Restarting...".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user