mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-29 22:56:45 +08:00
feat(web): 改为通过 WebSocket 推送 ttyd 状态并清理轮询与冗余接口
This commit is contained in:
@@ -7,7 +7,7 @@ pub mod types;
|
||||
|
||||
pub use types::{
|
||||
AtxDeviceInfo, AudioDeviceInfo, ClientStats, HidDeviceInfo, MsdDeviceInfo, SystemEvent,
|
||||
VideoDeviceInfo,
|
||||
TtydDeviceInfo, VideoDeviceInfo,
|
||||
};
|
||||
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
@@ -100,6 +100,15 @@ pub struct AudioDeviceInfo {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// ttyd status information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TtydDeviceInfo {
|
||||
/// Whether ttyd binary is available
|
||||
pub available: bool,
|
||||
/// Whether ttyd is currently running
|
||||
pub running: bool,
|
||||
}
|
||||
|
||||
/// Per-client statistics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientStats {
|
||||
@@ -325,6 +334,8 @@ pub enum SystemEvent {
|
||||
atx: Option<AtxDeviceInfo>,
|
||||
/// Audio device information (None if audio not enabled)
|
||||
audio: Option<AudioDeviceInfo>,
|
||||
/// ttyd status information
|
||||
ttyd: TtydDeviceInfo,
|
||||
},
|
||||
|
||||
/// WebSocket error notification (for connection-level errors like lag)
|
||||
|
||||
@@ -10,6 +10,7 @@ use tokio::process::{Child, Command};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::types::*;
|
||||
use crate::events::EventBus;
|
||||
|
||||
/// Maximum number of log lines to keep per extension
|
||||
const LOG_BUFFER_SIZE: usize = 200;
|
||||
@@ -31,6 +32,7 @@ pub struct ExtensionManager {
|
||||
processes: RwLock<HashMap<ExtensionId, ExtensionProcess>>,
|
||||
/// Cached availability status (checked once at startup)
|
||||
availability: HashMap<ExtensionId, bool>,
|
||||
event_bus: RwLock<Option<Arc<EventBus>>>,
|
||||
}
|
||||
|
||||
impl Default for ExtensionManager {
|
||||
@@ -51,6 +53,22 @@ impl ExtensionManager {
|
||||
Self {
|
||||
processes: RwLock::new(HashMap::new()),
|
||||
availability,
|
||||
event_bus: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set event bus for ttyd status notifications.
|
||||
pub async fn set_event_bus(&self, event_bus: Arc<EventBus>) {
|
||||
*self.event_bus.write().await = Some(event_bus);
|
||||
}
|
||||
|
||||
async fn mark_ttyd_status_dirty(&self, id: ExtensionId) {
|
||||
if id != ExtensionId::Ttyd {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(ref event_bus) = *self.event_bus.read().await {
|
||||
event_bus.mark_device_info_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,17 +83,38 @@ impl ExtensionManager {
|
||||
return ExtensionStatus::Unavailable;
|
||||
}
|
||||
|
||||
let processes = self.processes.read().await;
|
||||
match processes.get(&id) {
|
||||
Some(proc) => {
|
||||
if let Some(pid) = proc.child.id() {
|
||||
ExtensionStatus::Running { pid }
|
||||
} else {
|
||||
ExtensionStatus::Stopped
|
||||
let mut processes = self.processes.write().await;
|
||||
let exited = {
|
||||
let Some(proc) = processes.get_mut(&id) else {
|
||||
return ExtensionStatus::Stopped;
|
||||
};
|
||||
|
||||
match proc.child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
tracing::info!("Extension {} exited with status {}", id, status);
|
||||
true
|
||||
}
|
||||
Ok(None) => {
|
||||
return match proc.child.id() {
|
||||
Some(pid) => ExtensionStatus::Running { pid },
|
||||
None => ExtensionStatus::Stopped,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to query status for {}: {}", id, e);
|
||||
return match proc.child.id() {
|
||||
Some(pid) => ExtensionStatus::Running { pid },
|
||||
None => ExtensionStatus::Stopped,
|
||||
};
|
||||
}
|
||||
}
|
||||
None => ExtensionStatus::Stopped,
|
||||
};
|
||||
|
||||
if exited {
|
||||
processes.remove(&id);
|
||||
}
|
||||
|
||||
ExtensionStatus::Stopped
|
||||
}
|
||||
|
||||
/// Start an extension with the given configuration
|
||||
@@ -134,6 +173,8 @@ impl ExtensionManager {
|
||||
|
||||
let mut processes = self.processes.write().await;
|
||||
processes.insert(id, ExtensionProcess { child, logs });
|
||||
drop(processes);
|
||||
self.mark_ttyd_status_dirty(id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -146,6 +187,8 @@ impl ExtensionManager {
|
||||
if let Err(e) = proc.child.kill().await {
|
||||
tracing::warn!("Failed to kill {}: {}", id, e);
|
||||
}
|
||||
drop(processes);
|
||||
self.mark_ttyd_status_dirty(id).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1371,8 +1371,7 @@ mod tests {
|
||||
|
||||
// Test keyboard packet (8 bytes data)
|
||||
let data = [0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; // 'A' key
|
||||
let packet =
|
||||
Ch9329Backend::build_packet(DEFAULT_ADDR, cmd::SEND_KB_GENERAL_DATA, &data);
|
||||
let packet = Ch9329Backend::build_packet(DEFAULT_ADDR, cmd::SEND_KB_GENERAL_DATA, &data);
|
||||
|
||||
assert_eq!(packet[0], 0x57); // Header
|
||||
assert_eq!(packet[1], 0xAB); // Header
|
||||
|
||||
@@ -199,7 +199,12 @@ pub fn encode_keyboard_event(event: &KeyboardEvent) -> Vec<u8> {
|
||||
|
||||
let modifiers = event.modifiers.to_hid_byte();
|
||||
|
||||
vec![MSG_KEYBOARD, event_type, event.key.to_hid_usage(), modifiers]
|
||||
vec![
|
||||
MSG_KEYBOARD,
|
||||
event_type,
|
||||
event.key.to_hid_usage(),
|
||||
modifiers,
|
||||
]
|
||||
}
|
||||
|
||||
/// Encode a mouse event to binary format (for sending to client if needed)
|
||||
|
||||
@@ -576,6 +576,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
data_dir.clone(),
|
||||
);
|
||||
|
||||
extensions.set_event_bus(events.clone()).await;
|
||||
|
||||
// Start RustDesk service if enabled
|
||||
if let Some(ref service) = rustdesk {
|
||||
if let Err(e) = service.start().await {
|
||||
|
||||
18
src/state.rs
18
src/state.rs
@@ -7,9 +7,9 @@ use crate::auth::{SessionStore, UserStore};
|
||||
use crate::config::ConfigStore;
|
||||
use crate::events::{
|
||||
AtxDeviceInfo, AudioDeviceInfo, EventBus, HidDeviceInfo, MsdDeviceInfo, SystemEvent,
|
||||
VideoDeviceInfo,
|
||||
TtydDeviceInfo, VideoDeviceInfo,
|
||||
};
|
||||
use crate::extensions::ExtensionManager;
|
||||
use crate::extensions::{ExtensionId, ExtensionManager};
|
||||
use crate::hid::HidController;
|
||||
use crate::msd::MsdController;
|
||||
use crate::otg::OtgService;
|
||||
@@ -157,12 +157,13 @@ impl AppState {
|
||||
/// Uses tokio::join! to collect all device info in parallel for better performance.
|
||||
pub async fn get_device_info(&self) -> SystemEvent {
|
||||
// Collect all device info in parallel
|
||||
let (video, hid, msd, atx, audio) = tokio::join!(
|
||||
let (video, hid, msd, atx, audio, ttyd) = tokio::join!(
|
||||
self.collect_video_info(),
|
||||
self.collect_hid_info(),
|
||||
self.collect_msd_info(),
|
||||
self.collect_atx_info(),
|
||||
self.collect_audio_info(),
|
||||
self.collect_ttyd_info(),
|
||||
);
|
||||
|
||||
SystemEvent::DeviceInfo {
|
||||
@@ -171,6 +172,7 @@ impl AppState {
|
||||
msd,
|
||||
atx,
|
||||
audio,
|
||||
ttyd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,4 +264,14 @@ impl AppState {
|
||||
error: status.error,
|
||||
})
|
||||
}
|
||||
|
||||
/// Collect ttyd status information
|
||||
async fn collect_ttyd_info(&self) -> TtydDeviceInfo {
|
||||
let status = self.extensions.status(ExtensionId::Ttyd).await;
|
||||
|
||||
TtydDeviceInfo {
|
||||
available: self.extensions.check_available(ExtensionId::Ttyd),
|
||||
running: status.is_running(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,10 @@ fn log_encoding_error(
|
||||
if throttler.should_log(&key) {
|
||||
let suppressed = suppressed_errors.remove(&key).unwrap_or(0);
|
||||
if suppressed > 0 {
|
||||
error!("Encoding failed: {} (suppressed {} repeats)", err, suppressed);
|
||||
error!(
|
||||
"Encoding failed: {} (suppressed {} repeats)",
|
||||
err, suppressed
|
||||
);
|
||||
} else {
|
||||
error!("Encoding failed: {}", err);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,9 @@ impl MjpegDecoderKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build_encoder_state(config: &SharedVideoPipelineConfig) -> Result<EncoderThreadState> {
|
||||
pub(super) fn build_encoder_state(
|
||||
config: &SharedVideoPipelineConfig,
|
||||
) -> Result<EncoderThreadState> {
|
||||
let registry = EncoderRegistry::global();
|
||||
|
||||
let get_codec_name =
|
||||
@@ -408,8 +410,10 @@ pub(super) fn build_encoder_state(config: &SharedVideoPipelineConfig) -> Result<
|
||||
backend, codec_name
|
||||
);
|
||||
}
|
||||
let encoder =
|
||||
VP8Encoder::with_codec(VP8Config::low_latency(config.resolution, config.bitrate_kbps()), &codec_name)?;
|
||||
let encoder = VP8Encoder::with_codec(
|
||||
VP8Config::low_latency(config.resolution, config.bitrate_kbps()),
|
||||
&codec_name,
|
||||
)?;
|
||||
info!("Created VP8 encoder: {}", encoder.codec_name());
|
||||
Box::new(VP8EncoderWrapper(encoder))
|
||||
}
|
||||
@@ -421,8 +425,10 @@ pub(super) fn build_encoder_state(config: &SharedVideoPipelineConfig) -> Result<
|
||||
backend, codec_name
|
||||
);
|
||||
}
|
||||
let encoder =
|
||||
VP9Encoder::with_codec(VP9Config::low_latency(config.resolution, config.bitrate_kbps()), &codec_name)?;
|
||||
let encoder = VP9Encoder::with_codec(
|
||||
VP9Config::low_latency(config.resolution, config.bitrate_kbps()),
|
||||
&codec_name,
|
||||
)?;
|
||||
info!("Created VP9 encoder: {}", encoder.codec_name());
|
||||
Box::new(VP9EncoderWrapper(encoder))
|
||||
}
|
||||
@@ -505,7 +511,10 @@ pub(super) fn build_encoder_state(config: &SharedVideoPipelineConfig) -> Result<
|
||||
})
|
||||
}
|
||||
|
||||
fn h264_direct_input_format(codec_name: &str, input_format: PixelFormat) -> Option<H264InputFormat> {
|
||||
fn h264_direct_input_format(
|
||||
codec_name: &str,
|
||||
input_format: PixelFormat,
|
||||
) -> Option<H264InputFormat> {
|
||||
if codec_name.contains("rkmpp") {
|
||||
match input_format {
|
||||
PixelFormat::Yuyv => Some(H264InputFormat::Yuyv422),
|
||||
@@ -531,7 +540,10 @@ fn h264_direct_input_format(codec_name: &str, input_format: PixelFormat) -> Opti
|
||||
}
|
||||
}
|
||||
|
||||
fn h265_direct_input_format(codec_name: &str, input_format: PixelFormat) -> Option<H265InputFormat> {
|
||||
fn h265_direct_input_format(
|
||||
codec_name: &str,
|
||||
input_format: PixelFormat,
|
||||
) -> Option<H265InputFormat> {
|
||||
if codec_name.contains("rkmpp") {
|
||||
match input_format {
|
||||
PixelFormat::Yuyv => Some(H265InputFormat::Yuyv422),
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use typeshare::typeshare;
|
||||
|
||||
@@ -324,27 +324,3 @@ pub async fn update_easytier_config(
|
||||
|
||||
Ok(Json(new_config.extensions.easytier.clone()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ttyd status for console (simplified)
|
||||
// ============================================================================
|
||||
|
||||
/// Simple ttyd status for console view
|
||||
#[typeshare]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TtydStatus {
|
||||
pub available: bool,
|
||||
pub running: bool,
|
||||
}
|
||||
|
||||
/// Get ttyd status for console view
|
||||
/// GET /api/extensions/ttyd/status
|
||||
pub async fn get_ttyd_status(State(state): State<Arc<AppState>>) -> Json<TtydStatus> {
|
||||
let mgr = &state.extensions;
|
||||
let status = mgr.status(ExtensionId::Ttyd).await;
|
||||
|
||||
Json(TtydStatus {
|
||||
available: mgr.check_available(ExtensionId::Ttyd),
|
||||
running: status.is_running(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -196,10 +196,6 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
"/extensions/ttyd/config",
|
||||
patch(handlers::extensions::update_ttyd_config),
|
||||
)
|
||||
.route(
|
||||
"/extensions/ttyd/status",
|
||||
get(handlers::extensions::get_ttyd_status),
|
||||
)
|
||||
.route(
|
||||
"/extensions/gostc/config",
|
||||
patch(handlers::extensions::update_gostc_config),
|
||||
|
||||
Reference in New Issue
Block a user