fix: 修复扩展服务开启和自启动状态错误

This commit is contained in:
mofeng-git
2026-07-17 10:46:34 +08:00
parent 57d1597264
commit d35021ee5d
12 changed files with 430 additions and 136 deletions

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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();

View File

@@ -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...");
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,29 +551,25 @@ 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 {
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))
})?;
@@ -531,9 +577,25 @@ pub async fn apply_rustdesk_config(
"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...");
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,15 +666,6 @@ pub async fn apply_vnc_config(
}
let mut vnc_guard = state.vnc.write().await;
if !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
@@ -604,20 +673,44 @@ pub async fn apply_vnc_config(
|| 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 let Some(ref service) = *vnc_guard {
service.stop().await?;
}
*vnc_guard = None;
}
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 {
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?;
}
}
@@ -637,21 +730,18 @@ pub async fn apply_rtsp_config(
) -> Result<()> {
tracing::info!("Applying RTSP config changes...");
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;
if !new_config.enabled {
if let Some(ref service) = *rtsp_guard {
service
.stop()
.await
.map_err(|e| AppError::Config(format!("Failed to stop RTSP service: {}", e)))?;
}
*rtsp_guard = None;
}
if new_config.enabled {
let need_restart = options.force
|| old_config.bind != new_config.bind
|| old_config.port != new_config.port
@@ -661,16 +751,46 @@ pub async fn apply_rtsp_config(
|| 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 let Some(ref service) = *rtsp_guard {
service
.stop()
.await
.map_err(|e| AppError::Config(format!("Failed to stop RTSP service: {}", e)))?;
}
*rtsp_guard = None;
}
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 {
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?;
}
}

View File

@@ -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)))

View File

@@ -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))
}

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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>