mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 16:51:44 +08:00
fix: 修复扩展服务开启和自启动状态错误
This commit is contained in:
@@ -643,8 +643,12 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
{
|
||||
let runtime_config = state.config.get();
|
||||
let runtime_config = state.runtime_third_party_config().await;
|
||||
let constraints = StreamCodecConstraints::from_config(&runtime_config);
|
||||
state
|
||||
.stream_manager
|
||||
.set_runtime_codec_constraints(constraints.clone())
|
||||
.await;
|
||||
match enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await {
|
||||
Ok(result) if result.changed => {
|
||||
if let Some(message) = result.message {
|
||||
|
||||
@@ -2,9 +2,11 @@ use rtsp_types as rtsp;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::config::RtspConfig;
|
||||
use crate::error::{AppError, Result};
|
||||
@@ -25,6 +27,8 @@ use super::types::RtspConnectionState;
|
||||
|
||||
pub use super::types::RtspServiceStatus;
|
||||
|
||||
const RTSP_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
pub struct RtspService {
|
||||
config: Arc<RwLock<RtspConfig>>,
|
||||
status: Arc<RwLock<RtspServiceStatus>>,
|
||||
@@ -74,18 +78,37 @@ impl RtspService {
|
||||
tracing::debug!("Failed to request keyframe on RTSP start: {}", err);
|
||||
}
|
||||
|
||||
let bind_addr = bind_socket_addr(&config.bind, config.port)
|
||||
.map_err(|e| AppError::BadRequest(format!("Invalid RTSP bind address: {}", e)))?;
|
||||
let bind_addr = match bind_socket_addr(&config.bind, config.port) {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
let error = AppError::BadRequest(format!("Invalid RTSP bind address: {}", err));
|
||||
*self.status.write().await = RtspServiceStatus::Error(error.to_string());
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let listener = bind_tcp_listener(bind_addr).map_err(|e| {
|
||||
AppError::Io(io::Error::new(e.kind(), format!("RTSP bind failed: {}", e)))
|
||||
})?;
|
||||
let listener = TcpListener::from_std(listener).map_err(|e| {
|
||||
AppError::Io(io::Error::new(
|
||||
e.kind(),
|
||||
format!("RTSP listener setup failed: {}", e),
|
||||
))
|
||||
})?;
|
||||
let listener = match bind_tcp_listener(bind_addr) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
let error = AppError::Io(io::Error::new(
|
||||
err.kind(),
|
||||
format!("RTSP bind failed: {}", err),
|
||||
));
|
||||
*self.status.write().await = RtspServiceStatus::Error(error.to_string());
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let listener = match TcpListener::from_std(listener) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
let error = AppError::Io(io::Error::new(
|
||||
err.kind(),
|
||||
format!("RTSP listener setup failed: {}", err),
|
||||
));
|
||||
*self.status.write().await = RtspServiceStatus::Error(error.to_string());
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let service_config = self.config.clone();
|
||||
let video_manager = self.video_manager.clone();
|
||||
@@ -138,7 +161,7 @@ impl RtspService {
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
let _ = self.shutdown_tx.send(());
|
||||
if let Some(handle) = self.server_handle.lock().await.take() {
|
||||
handle.abort();
|
||||
wait_for_server_stop(handle).await;
|
||||
}
|
||||
|
||||
let mut client_handles = self.client_handles.lock().await;
|
||||
@@ -170,6 +193,42 @@ impl RtspService {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_server_stop(mut handle: JoinHandle<()>) {
|
||||
match tokio::time::timeout(RTSP_SHUTDOWN_TIMEOUT, &mut handle).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) if err.is_cancelled() => {}
|
||||
Ok(Err(err)) => tracing::warn!("RTSP server task ended with error: {}", err),
|
||||
Err(_) => {
|
||||
tracing::warn!("Timed out waiting for RTSP server task to stop");
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn waiting_for_server_stop_releases_listener() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("bind test listener");
|
||||
let bind_addr = listener.local_addr().expect("read test listener address");
|
||||
let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1);
|
||||
let handle = tokio::spawn(async move {
|
||||
let _listener = listener;
|
||||
let _ = shutdown_rx.recv().await;
|
||||
});
|
||||
|
||||
shutdown_tx.send(()).expect("send shutdown signal");
|
||||
wait_for_server_stop(handle).await;
|
||||
|
||||
std::net::TcpListener::bind(bind_addr).expect("rebind released listener address");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_client(
|
||||
mut stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
|
||||
@@ -32,6 +32,7 @@ use self::protocol::{make_local_addr, make_relay_response, make_request_relay};
|
||||
use self::rendezvous::{AddrMangle, RendezvousMediator, RendezvousStatus};
|
||||
|
||||
const RELAY_CONNECT_TIMEOUT_MS: u64 = 10_000;
|
||||
const SERVICE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ServiceStatus {
|
||||
@@ -121,6 +122,10 @@ impl RustDeskService {
|
||||
self.connection_manager.connection_count()
|
||||
}
|
||||
|
||||
pub fn is_listening(&self) -> bool {
|
||||
self.tcp_listener_handle.read().is_some()
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> anyhow::Result<()> {
|
||||
let config = self.config.read().clone();
|
||||
|
||||
@@ -169,7 +174,13 @@ impl RustDeskService {
|
||||
|
||||
*self.rendezvous.write() = Some(mediator.clone());
|
||||
|
||||
let (tcp_handles, listen_port) = self.start_tcp_listener_with_port().await?;
|
||||
let (tcp_handles, listen_port) = match self.start_tcp_listener_with_port().await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
*self.status.write() = ServiceStatus::Error(err.to_string());
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
*self.tcp_listener_handle.write() = Some(tcp_handles);
|
||||
|
||||
mediator.set_listen_port(listen_port);
|
||||
@@ -383,13 +394,15 @@ impl RustDeskService {
|
||||
mediator.stop();
|
||||
}
|
||||
|
||||
if let Some(handle) = self.rendezvous_handle.write().take() {
|
||||
handle.abort();
|
||||
let rendezvous_handle = self.rendezvous_handle.write().take();
|
||||
if let Some(handle) = rendezvous_handle {
|
||||
wait_for_service_task(handle, "rendezvous").await;
|
||||
}
|
||||
|
||||
if let Some(handles) = self.tcp_listener_handle.write().take() {
|
||||
let tcp_listener_handles = self.tcp_listener_handle.write().take();
|
||||
if let Some(handles) = tcp_listener_handles {
|
||||
for handle in handles {
|
||||
handle.abort();
|
||||
wait_for_service_task(handle, "TCP listener").await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +467,19 @@ impl RustDeskService {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_service_task(mut handle: JoinHandle<()>, task_name: &str) {
|
||||
match tokio::time::timeout(SERVICE_SHUTDOWN_TIMEOUT, &mut handle).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) if err.is_cancelled() => {}
|
||||
Ok(Err(err)) => warn!("RustDesk {} task ended with error: {}", task_name, err),
|
||||
Err(_) => {
|
||||
warn!("Timed out waiting for RustDesk {} task to stop", task_name);
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rustdesk_relay_key(config: &Arc<RwLock<RustDeskConfig>>) -> String {
|
||||
config.read().relay_key.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
27
src/state.rs
27
src/state.rs
@@ -150,6 +150,33 @@ impl AppState {
|
||||
&self.data_dir
|
||||
}
|
||||
|
||||
pub async fn runtime_third_party_config(&self) -> crate::config::AppConfig {
|
||||
let mut config = self.config.get().as_ref().clone();
|
||||
|
||||
config.rustdesk.enabled = self
|
||||
.rustdesk
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.is_some_and(|service| service.is_listening());
|
||||
config.vnc.enabled = match self.vnc.read().await.as_ref() {
|
||||
Some(service) => matches!(
|
||||
service.status().await,
|
||||
crate::vnc::VncServiceStatus::Starting | crate::vnc::VncServiceStatus::Running
|
||||
),
|
||||
None => false,
|
||||
};
|
||||
config.rtsp.enabled = match self.rtsp.read().await.as_ref() {
|
||||
Some(service) => matches!(
|
||||
service.status().await,
|
||||
crate::rtsp::RtspServiceStatus::Starting | crate::rtsp::RtspServiceStatus::Running
|
||||
),
|
||||
None => false,
|
||||
};
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn subscribe_device_info(&self) -> watch::Receiver<Option<SystemEvent>> {
|
||||
self.device_info_tx.subscribe()
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@ pub struct VideoStreamManager {
|
||||
events: RwLock<Option<Arc<EventBus>>>,
|
||||
/// Configuration store
|
||||
config_store: RwLock<Option<ConfigStore>>,
|
||||
/// Codec constraints derived from services that are actually running.
|
||||
runtime_codec_constraints: RwLock<Option<StreamCodecConstraints>>,
|
||||
/// Mode switching lock to prevent concurrent switch requests
|
||||
switching: AtomicBool,
|
||||
/// Current mode switch transaction ID (set while switching=true)
|
||||
@@ -118,6 +120,7 @@ impl VideoStreamManager {
|
||||
webrtc_streamer,
|
||||
events: RwLock::new(None),
|
||||
config_store: RwLock::new(None),
|
||||
runtime_codec_constraints: RwLock::new(None),
|
||||
switching: AtomicBool::new(false),
|
||||
transition_id: RwLock::new(None),
|
||||
})
|
||||
@@ -144,8 +147,16 @@ impl VideoStreamManager {
|
||||
*self.config_store.write().await = Some(config);
|
||||
}
|
||||
|
||||
/// Get current stream codec constraints derived from global configuration.
|
||||
pub async fn set_runtime_codec_constraints(&self, constraints: StreamCodecConstraints) {
|
||||
*self.runtime_codec_constraints.write().await = Some(constraints);
|
||||
}
|
||||
|
||||
/// Get current stream codec constraints derived from running services.
|
||||
pub async fn codec_constraints(&self) -> StreamCodecConstraints {
|
||||
if let Some(constraints) = self.runtime_codec_constraints.read().await.as_ref() {
|
||||
return constraints.clone();
|
||||
}
|
||||
|
||||
if let Some(ref config_store) = *self.config_store.read().await {
|
||||
let config = config_store.get();
|
||||
StreamCodecConstraints::from_config(&config)
|
||||
|
||||
@@ -121,20 +121,36 @@ impl VncService {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let bind_addr = bind_socket_addr(&config.bind, config.port)
|
||||
.map_err(|e| AppError::BadRequest(format!("Invalid VNC bind address: {}", e)))?;
|
||||
let listener = bind_tcp_listener(bind_addr).map_err(|e| {
|
||||
AppError::Io(std::io::Error::new(
|
||||
e.kind(),
|
||||
format!("VNC bind failed: {}", e),
|
||||
))
|
||||
})?;
|
||||
let listener = TcpListener::from_std(listener).map_err(|e| {
|
||||
AppError::Io(std::io::Error::new(
|
||||
e.kind(),
|
||||
format!("VNC listener setup failed: {}", e),
|
||||
))
|
||||
})?;
|
||||
let bind_addr = match bind_socket_addr(&config.bind, config.port) {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
let error = AppError::BadRequest(format!("Invalid VNC bind address: {}", err));
|
||||
*self.status.write().await = VncServiceStatus::Error(error.to_string());
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let listener = match bind_tcp_listener(bind_addr) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
let error = AppError::Io(std::io::Error::new(
|
||||
err.kind(),
|
||||
format!("VNC bind failed: {}", err),
|
||||
));
|
||||
*self.status.write().await = VncServiceStatus::Error(error.to_string());
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let listener = match TcpListener::from_std(listener) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
let error = AppError::Io(std::io::Error::new(
|
||||
err.kind(),
|
||||
format!("VNC listener setup failed: {}", err),
|
||||
));
|
||||
*self.status.write().await = VncServiceStatus::Error(error.to_string());
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let config_ref = self.config.clone();
|
||||
let video_manager = self.video_manager.clone();
|
||||
|
||||
@@ -14,11 +14,33 @@ use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ConfigApplyOptions {
|
||||
pub force: bool,
|
||||
pub preserve_service_state: bool,
|
||||
pub runtime_only: bool,
|
||||
}
|
||||
|
||||
impl ConfigApplyOptions {
|
||||
pub const fn forced() -> Self {
|
||||
Self { force: true }
|
||||
Self {
|
||||
force: true,
|
||||
preserve_service_state: false,
|
||||
runtime_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn preserving_service_state() -> Self {
|
||||
Self {
|
||||
force: false,
|
||||
preserve_service_state: true,
|
||||
runtime_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn runtime_only() -> Self {
|
||||
Self {
|
||||
force: false,
|
||||
preserve_service_state: false,
|
||||
runtime_only: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,13 +471,27 @@ pub async fn apply_audio_config(
|
||||
}
|
||||
|
||||
pub async fn enforce_stream_codec_constraints(state: &Arc<AppState>) -> Result<Option<String>> {
|
||||
let config = state.config.get();
|
||||
let config = state.runtime_third_party_config().await;
|
||||
let constraints = StreamCodecConstraints::from_config(&config);
|
||||
state
|
||||
.stream_manager
|
||||
.set_runtime_codec_constraints(constraints.clone())
|
||||
.await;
|
||||
let enforcement =
|
||||
enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await?;
|
||||
Ok(enforcement.message)
|
||||
}
|
||||
|
||||
async fn validate_runtime_candidate<T>(
|
||||
state: &Arc<AppState>,
|
||||
apply: impl FnOnce(&mut crate::config::AppConfig, T),
|
||||
config: T,
|
||||
) -> Result<()> {
|
||||
let mut candidate = state.runtime_third_party_config().await;
|
||||
apply(&mut candidate, config);
|
||||
validate_third_party_codec_compatibility(&candidate)
|
||||
}
|
||||
|
||||
fn validate_rustdesk_candidate(
|
||||
state: &Arc<AppState>,
|
||||
new_config: &crate::rustdesk::config::RustDeskConfig,
|
||||
@@ -485,12 +521,26 @@ pub async fn apply_rustdesk_config(
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying RustDesk config changes...");
|
||||
|
||||
validate_rustdesk_candidate(state, new_config)?;
|
||||
if options.runtime_only {
|
||||
validate_runtime_candidate(
|
||||
state,
|
||||
|candidate, config| candidate.rustdesk = config,
|
||||
new_config.clone(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
validate_rustdesk_candidate(state, new_config)?;
|
||||
}
|
||||
|
||||
let mut rustdesk_guard = state.rustdesk.write().await;
|
||||
let mut credentials_to_save = None;
|
||||
let need_restart = options.force
|
||||
|| old_config.codec != new_config.codec
|
||||
|| old_config.rendezvous_server != new_config.rendezvous_server
|
||||
|| old_config.device_id != new_config.device_id
|
||||
|| old_config.device_password != new_config.device_password;
|
||||
|
||||
if !new_config.enabled {
|
||||
if !options.preserve_service_state && !new_config.enabled {
|
||||
if let Some(ref service) = *rustdesk_guard {
|
||||
service
|
||||
.stop()
|
||||
@@ -501,39 +551,51 @@ pub async fn apply_rustdesk_config(
|
||||
*rustdesk_guard = None;
|
||||
}
|
||||
|
||||
if new_config.enabled {
|
||||
let need_restart = options.force
|
||||
|| old_config.codec != new_config.codec
|
||||
|| old_config.rendezvous_server != new_config.rendezvous_server
|
||||
|| old_config.device_id != new_config.device_id
|
||||
|| old_config.device_password != new_config.device_password;
|
||||
|
||||
if !options.preserve_service_state && new_config.enabled {
|
||||
if rustdesk_guard.is_none() {
|
||||
tracing::info!("Initializing RustDesk service...");
|
||||
let service = crate::rustdesk::RustDeskService::new(
|
||||
let service = std::sync::Arc::new(crate::rustdesk::RustDeskService::new(
|
||||
new_config.clone(),
|
||||
state.stream_manager.clone(),
|
||||
state.hid.clone(),
|
||||
state.audio.clone(),
|
||||
);
|
||||
));
|
||||
*rustdesk_guard = Some(service.clone());
|
||||
service.start().await.map_err(|e| {
|
||||
AppError::Config(format!("Failed to start RustDesk service: {}", e))
|
||||
})?;
|
||||
tracing::info!("RustDesk service started with ID: {}", new_config.device_id);
|
||||
credentials_to_save = service.save_credentials();
|
||||
*rustdesk_guard = Some(std::sync::Arc::new(service));
|
||||
} else if need_restart {
|
||||
} else {
|
||||
if let Some(ref service) = *rustdesk_guard {
|
||||
service.restart(new_config.clone()).await.map_err(|e| {
|
||||
AppError::Config(format!("Failed to restart RustDesk service: {}", e))
|
||||
})?;
|
||||
tracing::info!(
|
||||
"RustDesk service restarted with ID: {}",
|
||||
new_config.device_id
|
||||
);
|
||||
if service.is_listening() {
|
||||
if need_restart {
|
||||
service.restart(new_config.clone()).await.map_err(|e| {
|
||||
AppError::Config(format!("Failed to restart RustDesk service: {}", e))
|
||||
})?;
|
||||
tracing::info!(
|
||||
"RustDesk service restarted with ID: {}",
|
||||
new_config.device_id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
service.update_config(new_config.clone());
|
||||
service.start().await.map_err(|e| {
|
||||
AppError::Config(format!("Failed to start RustDesk service: {}", e))
|
||||
})?;
|
||||
}
|
||||
credentials_to_save = service.save_credentials();
|
||||
}
|
||||
}
|
||||
} else if options.preserve_service_state && need_restart {
|
||||
if let Some(ref service) = *rustdesk_guard {
|
||||
let mut runtime_config = new_config.clone();
|
||||
runtime_config.enabled = true;
|
||||
service.restart(runtime_config).await.map_err(|e| {
|
||||
AppError::Config(format!("Failed to restart RustDesk service: {}", e))
|
||||
})?;
|
||||
credentials_to_save = service.save_credentials();
|
||||
}
|
||||
}
|
||||
|
||||
drop(rustdesk_guard);
|
||||
@@ -567,11 +629,27 @@ pub async fn apply_vnc_config(
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying VNC config changes...");
|
||||
|
||||
validate_vnc_candidate(state, new_config)?;
|
||||
if options.runtime_only {
|
||||
validate_runtime_candidate(
|
||||
state,
|
||||
|candidate, config| candidate.vnc = config,
|
||||
new_config.clone(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
validate_vnc_candidate(state, new_config)?;
|
||||
}
|
||||
|
||||
if new_config.enabled {
|
||||
let mut candidate = state.config.get().as_ref().clone();
|
||||
let runtime_config = state.runtime_third_party_config().await;
|
||||
let will_run = if options.preserve_service_state {
|
||||
runtime_config.vnc.enabled
|
||||
} else {
|
||||
new_config.enabled
|
||||
};
|
||||
if will_run {
|
||||
let mut candidate = runtime_config;
|
||||
candidate.vnc = new_config.clone();
|
||||
candidate.vnc.enabled = true;
|
||||
let constraints = StreamCodecConstraints::from_config(&candidate);
|
||||
match enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await {
|
||||
Ok(result) if result.changed => {
|
||||
@@ -588,37 +666,52 @@ pub async fn apply_vnc_config(
|
||||
}
|
||||
|
||||
let mut vnc_guard = state.vnc.write().await;
|
||||
let need_restart = options.force
|
||||
|| old_config.bind != new_config.bind
|
||||
|| old_config.port != new_config.port
|
||||
|| old_config.encoding != new_config.encoding
|
||||
|| old_config.password != new_config.password
|
||||
|| old_config.allow_one_client != new_config.allow_one_client;
|
||||
|
||||
if !new_config.enabled {
|
||||
if !options.preserve_service_state && !new_config.enabled {
|
||||
if let Some(ref service) = *vnc_guard {
|
||||
service.stop().await?;
|
||||
}
|
||||
*vnc_guard = None;
|
||||
}
|
||||
|
||||
if new_config.enabled {
|
||||
let need_restart = options.force
|
||||
|| old_config.bind != new_config.bind
|
||||
|| old_config.port != new_config.port
|
||||
|| old_config.encoding != new_config.encoding
|
||||
|| old_config.password != new_config.password
|
||||
|| old_config.allow_one_client != new_config.allow_one_client;
|
||||
|
||||
if !options.preserve_service_state && new_config.enabled {
|
||||
if vnc_guard.is_none() {
|
||||
let service = crate::vnc::VncService::new(
|
||||
let service = Arc::new(crate::vnc::VncService::new(
|
||||
new_config.clone(),
|
||||
state.stream_manager.clone(),
|
||||
state.hid.clone(),
|
||||
);
|
||||
));
|
||||
*vnc_guard = Some(service.clone());
|
||||
service.start().await?;
|
||||
*vnc_guard = Some(Arc::new(service));
|
||||
tracing::info!("VNC service started");
|
||||
} else if need_restart {
|
||||
} else {
|
||||
if let Some(ref service) = *vnc_guard {
|
||||
service.restart(new_config.clone()).await?;
|
||||
tracing::info!("VNC service restarted");
|
||||
if matches!(
|
||||
service.status().await,
|
||||
crate::vnc::VncServiceStatus::Running
|
||||
) {
|
||||
if need_restart {
|
||||
service.restart(new_config.clone()).await?;
|
||||
tracing::info!("VNC service restarted");
|
||||
}
|
||||
} else {
|
||||
service.update_config(new_config.clone()).await;
|
||||
service.start().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if options.preserve_service_state && need_restart {
|
||||
if let Some(ref service) = *vnc_guard {
|
||||
let mut runtime_config = new_config.clone();
|
||||
runtime_config.enabled = true;
|
||||
service.restart(runtime_config).await?;
|
||||
}
|
||||
}
|
||||
|
||||
drop(vnc_guard);
|
||||
@@ -637,11 +730,28 @@ pub async fn apply_rtsp_config(
|
||||
) -> Result<()> {
|
||||
tracing::info!("Applying RTSP config changes...");
|
||||
|
||||
validate_rtsp_candidate(state, new_config)?;
|
||||
if options.runtime_only {
|
||||
validate_runtime_candidate(
|
||||
state,
|
||||
|candidate, config| candidate.rtsp = config,
|
||||
new_config.clone(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
validate_rtsp_candidate(state, new_config)?;
|
||||
}
|
||||
|
||||
let mut rtsp_guard = state.rtsp.write().await;
|
||||
let need_restart = options.force
|
||||
|| old_config.bind != new_config.bind
|
||||
|| old_config.port != new_config.port
|
||||
|| old_config.path != new_config.path
|
||||
|| old_config.codec != new_config.codec
|
||||
|| old_config.username != new_config.username
|
||||
|| old_config.password != new_config.password
|
||||
|| old_config.allow_one_client != new_config.allow_one_client;
|
||||
|
||||
if !new_config.enabled {
|
||||
if !options.preserve_service_state && !new_config.enabled {
|
||||
if let Some(ref service) = *rtsp_guard {
|
||||
service
|
||||
.stop()
|
||||
@@ -651,27 +761,37 @@ pub async fn apply_rtsp_config(
|
||||
*rtsp_guard = None;
|
||||
}
|
||||
|
||||
if new_config.enabled {
|
||||
let need_restart = options.force
|
||||
|| old_config.bind != new_config.bind
|
||||
|| old_config.port != new_config.port
|
||||
|| old_config.path != new_config.path
|
||||
|| old_config.codec != new_config.codec
|
||||
|| old_config.username != new_config.username
|
||||
|| old_config.password != new_config.password
|
||||
|| old_config.allow_one_client != new_config.allow_one_client;
|
||||
|
||||
if !options.preserve_service_state && new_config.enabled {
|
||||
if rtsp_guard.is_none() {
|
||||
let service = RtspService::new(new_config.clone(), state.stream_manager.clone());
|
||||
let service = Arc::new(RtspService::new(
|
||||
new_config.clone(),
|
||||
state.stream_manager.clone(),
|
||||
));
|
||||
*rtsp_guard = Some(service.clone());
|
||||
service.start().await?;
|
||||
tracing::info!("RTSP service started");
|
||||
*rtsp_guard = Some(Arc::new(service));
|
||||
} else if need_restart {
|
||||
} else {
|
||||
if let Some(ref service) = *rtsp_guard {
|
||||
service.restart(new_config.clone()).await?;
|
||||
tracing::info!("RTSP service restarted");
|
||||
if matches!(
|
||||
service.status().await,
|
||||
crate::rtsp::RtspServiceStatus::Running
|
||||
) {
|
||||
if need_restart {
|
||||
service.restart(new_config.clone()).await?;
|
||||
tracing::info!("RTSP service restarted");
|
||||
}
|
||||
} else {
|
||||
service.update_config(new_config.clone()).await;
|
||||
service.start().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if options.preserve_service_state && need_restart {
|
||||
if let Some(ref service) = *rtsp_guard {
|
||||
let mut runtime_config = new_config.clone();
|
||||
runtime_config.enabled = true;
|
||||
service.restart(runtime_config).await?;
|
||||
}
|
||||
}
|
||||
|
||||
drop(rtsp_guard);
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn persist_and_apply(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
ConfigApplyOptions::preserving_service_state(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
@@ -76,10 +76,17 @@ pub async fn start_rtsp_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<RtspStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?;
|
||||
let current_config = state.config.get().rtsp.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
let stored_config = state.config.get().rtsp.clone();
|
||||
let runtime_config = state.runtime_third_party_config().await.rtsp;
|
||||
let mut start_config = stored_config.clone();
|
||||
start_config.enabled = true;
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
&runtime_config,
|
||||
&start_config,
|
||||
ConfigApplyOptions::runtime_only(),
|
||||
)
|
||||
.await?;
|
||||
let status = current_status(&state).await;
|
||||
|
||||
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
|
||||
@@ -89,11 +96,17 @@ pub async fn stop_rtsp_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<RtspStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?;
|
||||
let current_config = state.config.get().rtsp.clone();
|
||||
let mut stop_config = current_config.clone();
|
||||
let stored_config = state.config.get().rtsp.clone();
|
||||
let runtime_config = state.runtime_third_party_config().await.rtsp;
|
||||
let mut stop_config = stored_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
apply_rtsp_config(
|
||||
&state,
|
||||
&runtime_config,
|
||||
&stop_config,
|
||||
ConfigApplyOptions::runtime_only(),
|
||||
)
|
||||
.await?;
|
||||
let status = current_status(&state).await;
|
||||
|
||||
Ok(Json(RtspStatusResponse::new(&stored_config, status)))
|
||||
|
||||
@@ -31,7 +31,7 @@ async fn persist_and_apply(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
ConfigApplyOptions::preserving_service_state(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
@@ -168,10 +168,18 @@ pub async fn start_rustdesk_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<RustDeskStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?;
|
||||
let current_config = state.config.get().rustdesk.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
let stored_config = state.config.get().rustdesk.clone();
|
||||
let runtime_config = state.runtime_third_party_config().await.rustdesk;
|
||||
let mut start_config = stored_config.clone();
|
||||
start_config.enabled = true;
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
&runtime_config,
|
||||
&start_config,
|
||||
ConfigApplyOptions::runtime_only(),
|
||||
)
|
||||
.await?;
|
||||
let stored_config = state.config.get().rustdesk.clone();
|
||||
Ok(Json(current_status(&state, stored_config).await))
|
||||
}
|
||||
|
||||
@@ -179,10 +187,16 @@ pub async fn stop_rustdesk_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<RustDeskStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?;
|
||||
let current_config = state.config.get().rustdesk.clone();
|
||||
let mut stop_config = current_config.clone();
|
||||
let stored_config = state.config.get().rustdesk.clone();
|
||||
let runtime_config = state.runtime_third_party_config().await.rustdesk;
|
||||
let mut stop_config = stored_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
apply_rustdesk_config(
|
||||
&state,
|
||||
&runtime_config,
|
||||
&stop_config,
|
||||
ConfigApplyOptions::runtime_only(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(current_status(&state, stored_config).await))
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn persist_and_apply(
|
||||
state,
|
||||
&old_config,
|
||||
&stored_config,
|
||||
ConfigApplyOptions::forced(),
|
||||
ConfigApplyOptions::preserving_service_state(),
|
||||
)
|
||||
.await?;
|
||||
Ok(stored_config)
|
||||
@@ -76,13 +76,20 @@ pub async fn start_vnc_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<VncStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let current_config = state.config.get().vnc.clone();
|
||||
let mut start_config = current_config.clone();
|
||||
let stored_config = state.config.get().vnc.clone();
|
||||
let runtime_config = state.runtime_third_party_config().await.vnc;
|
||||
let mut start_config = stored_config.clone();
|
||||
start_config.enabled = true;
|
||||
if start_config.password.as_deref().unwrap_or("").is_empty() {
|
||||
start_config.password = current_config.password.clone();
|
||||
start_config.password = stored_config.password.clone();
|
||||
}
|
||||
let stored_config = persist_and_apply(&state, current_config, start_config).await?;
|
||||
apply_vnc_config(
|
||||
&state,
|
||||
&runtime_config,
|
||||
&start_config,
|
||||
ConfigApplyOptions::runtime_only(),
|
||||
)
|
||||
.await?;
|
||||
let (status, connection_count) = current_status(&state).await;
|
||||
|
||||
Ok(Json(VncStatusResponse::new(
|
||||
@@ -96,11 +103,17 @@ pub async fn stop_vnc_service(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<VncStatusResponse>> {
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.vnc, "vnc")?;
|
||||
let current_config = state.config.get().vnc.clone();
|
||||
let mut stop_config = current_config.clone();
|
||||
let stored_config = state.config.get().vnc.clone();
|
||||
let runtime_config = state.runtime_third_party_config().await.vnc;
|
||||
let mut stop_config = stored_config.clone();
|
||||
stop_config.enabled = false;
|
||||
|
||||
let stored_config = persist_and_apply(&state, current_config, stop_config).await?;
|
||||
apply_vnc_config(
|
||||
&state,
|
||||
&runtime_config,
|
||||
&stop_config,
|
||||
ConfigApplyOptions::runtime_only(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(VncStatusResponse::new(
|
||||
&stored_config,
|
||||
|
||||
@@ -74,18 +74,9 @@ const loadingCodecs = ref(false)
|
||||
const backends = ref<EncoderBackendInfo[]>([])
|
||||
const constraints = ref<StreamConstraintsResponse | null>(null)
|
||||
const currentEncoderBackend = computed(() => configStore.stream?.encoder || 'auto')
|
||||
const isRtspEnabled = computed(() => {
|
||||
if (typeof configStore.rtspStatus?.config?.enabled === 'boolean') {
|
||||
return configStore.rtspStatus.config.enabled
|
||||
}
|
||||
return !!configStore.rtspConfig?.enabled
|
||||
})
|
||||
const isRustdeskEnabled = computed(() => {
|
||||
if (typeof configStore.rustdeskStatus?.config?.enabled === 'boolean') {
|
||||
return configStore.rustdeskStatus.config.enabled
|
||||
}
|
||||
return !!configStore.rustdeskConfig?.enabled
|
||||
})
|
||||
const isServiceActive = (status: string | undefined) => status === 'starting' || status === 'running'
|
||||
const isRtspEnabled = computed(() => isServiceActive(configStore.rtspStatus?.service_status))
|
||||
const isRustdeskEnabled = computed(() => isServiceActive(configStore.rustdeskStatus?.service_status))
|
||||
const isRtspCodecLocked = computed(() => isRtspEnabled.value)
|
||||
const isRustdeskWebrtcLocked = computed(() => !isRtspEnabled.value && isRustdeskEnabled.value)
|
||||
const codecLockSources = computed(() => {
|
||||
|
||||
@@ -2376,7 +2376,7 @@ async function startRustdesk() {
|
||||
|
||||
rustdeskLoading.value = true
|
||||
try {
|
||||
await configStore.updateRustdesk(rustdeskUpdatePayload(true))
|
||||
await configStore.updateRustdesk(rustdeskUpdatePayload())
|
||||
const status = await rustdeskConfigApi.start()
|
||||
applyRustdeskStatus(status)
|
||||
} catch {
|
||||
@@ -2501,7 +2501,7 @@ async function saveRtspConfig() {
|
||||
async function startRtsp() {
|
||||
rtspLoading.value = true
|
||||
try {
|
||||
await configStore.updateRtsp(rtspUpdatePayload(true))
|
||||
await configStore.updateRtsp(rtspUpdatePayload())
|
||||
const status = await rtspConfigApi.start()
|
||||
applyRtspStatus(status)
|
||||
} catch {
|
||||
@@ -2578,7 +2578,7 @@ async function startVnc() {
|
||||
|
||||
vncLoading.value = true
|
||||
try {
|
||||
await configStore.updateVnc(vncUpdatePayload(true))
|
||||
await configStore.updateVnc(vncUpdatePayload())
|
||||
const status = await vncConfigApi.start()
|
||||
applyVncStatus(status)
|
||||
} catch {
|
||||
@@ -4714,7 +4714,7 @@ watch(isWindows, () => {
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch v-model="rtspLocalConfig.enabled" :disabled="rtspStatus?.service_status === 'running'" />
|
||||
<Switch v-model="rtspLocalConfig.enabled" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.rtsp.bind') }}</Label>
|
||||
@@ -4781,7 +4781,7 @@ watch(isWindows, () => {
|
||||
<span class="shrink-0 font-medium">{{ t('extensions.rtsp.urlPreview') }}</span>
|
||||
<code class="truncate font-mono">{{ rtspStreamUrl }}</code>
|
||||
</div>
|
||||
<Button class="shrink-0" :disabled="loading || rtspLoading || rtspStatus?.service_status === 'running'" @click="saveRtspConfig">
|
||||
<Button class="shrink-0" :disabled="loading || rtspLoading" @click="saveRtspConfig">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@@ -4844,7 +4844,7 @@ watch(isWindows, () => {
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch v-model="vncLocalConfig.enabled" :disabled="vncStatus?.service_status === 'running'" />
|
||||
<Switch v-model="vncLocalConfig.enabled" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.vnc.bind') }}</Label>
|
||||
@@ -4903,7 +4903,7 @@ watch(isWindows, () => {
|
||||
<span class="shrink-0 font-medium">{{ t('extensions.vnc.urlPreview') }}</span>
|
||||
<code class="truncate font-mono">{{ vncStreamUrl }}</code>
|
||||
</div>
|
||||
<Button class="shrink-0" :disabled="loading || vncLoading || vncStatus?.service_status === 'running'" @click="saveVncConfig">
|
||||
<Button class="shrink-0" :disabled="loading || vncLoading" @click="saveVncConfig">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@@ -4969,7 +4969,7 @@ watch(isWindows, () => {
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch v-model="rustdeskLocalConfig.enabled" :disabled="rustdeskStatus?.service_status === 'running'" />
|
||||
<Switch v-model="rustdeskLocalConfig.enabled" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.rustdesk.codec') }}</Label>
|
||||
@@ -5093,7 +5093,7 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="border-t pt-4 justify-end">
|
||||
<Button :disabled="loading || rustdeskStatus?.service_status === 'running'" @click="saveRustdeskConfig">
|
||||
<Button :disabled="loading || rustdeskLoading" @click="saveRustdeskConfig">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
Reference in New Issue
Block a user