Skip to main content

spvirit_server/
server.rs

1//! Top-level PVA server orchestration.
2//!
3//! [`run_pva_server`] binds UDP + TCP + beacon and runs until cancelled.
4
5use std::net::{IpAddr, Ipv4Addr, SocketAddr};
6use std::sync::Arc;
7use std::time::Duration;
8
9use regex::Regex;
10use tracing::{error, info};
11
12use crate::beacon::{BeaconConfig, run_beacon};
13use tokio::net::TcpListener;
14
15use crate::handler::{PvListMode, ServerState, rand_guid, run_tcp_server, run_udp_search};
16use crate::monitor::MonitorRegistry;
17use crate::pvstore::SourceRegistry;
18
19/// Configuration for the PVA server.
20pub struct PvaServerConfig {
21    /// IP address to listen on (default: 0.0.0.0).
22    pub listen_ip: IpAddr,
23    /// TCP port (default: 5075).
24    pub tcp_port: u16,
25    /// UDP port (default: 5076).
26    pub udp_port: u16,
27    /// Address to advertise in search responses (None = auto).
28    pub advertise_ip: Option<IpAddr>,
29    /// Beacon target address (default: 224.0.0.128:5076).
30    pub beacon_target: SocketAddr,
31    /// Beacon period in seconds.
32    pub beacon_period_secs: u64,
33    /// Idle connection timeout.
34    pub conn_timeout: Duration,
35    /// Whether to compute alarms from limits.
36    pub compute_alarms: bool,
37    /// PV list mode.
38    pub pvlist_mode: PvListMode,
39    /// Maximum PV names in pvlist responses.
40    pub pvlist_max: usize,
41    /// Optional regex filter for pvlist.
42    pub pvlist_allow_pattern: Option<Regex>,
43}
44
45impl Default for PvaServerConfig {
46    fn default() -> Self {
47        Self {
48            listen_ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
49            tcp_port: 5075,
50            udp_port: 5076,
51            advertise_ip: None,
52            beacon_target: "224.0.0.128:5076".parse().unwrap(),
53            beacon_period_secs: 15,
54            conn_timeout: Duration::from_secs(64000),
55            compute_alarms: false,
56            pvlist_mode: PvListMode::List,
57            pvlist_max: 1024,
58            pvlist_allow_pattern: None,
59        }
60    }
61}
62
63/// Shared server state wrapping a [`SourceRegistry`].
64///
65/// Consumers can hold an `Arc<PvaServerState>` to inspect or mutate the
66/// underlying sources while the server tasks are running.
67pub struct PvaServerState {
68    pub inner: Arc<ServerState>,
69    pub registry: Arc<MonitorRegistry>,
70}
71
72impl PvaServerState {
73    pub fn new(sources: Arc<SourceRegistry>, config: &PvaServerConfig) -> Self {
74        Self::with_registry(sources, config, Arc::new(MonitorRegistry::new()))
75    }
76
77    pub fn with_registry(
78        sources: Arc<SourceRegistry>,
79        config: &PvaServerConfig,
80        registry: Arc<MonitorRegistry>,
81    ) -> Self {
82        let guid = rand_guid();
83        let inner = Arc::new(ServerState::new(
84            sources,
85            registry.clone(),
86            config.compute_alarms,
87            config.pvlist_mode,
88            config.pvlist_max,
89            config.pvlist_allow_pattern.clone(),
90            guid,
91            config.tcp_port,
92            config.advertise_ip,
93            config.listen_ip,
94        ));
95        Self { inner, registry }
96    }
97}
98
99/// Run a PVA server (UDP search + TCP handler + beacon).
100///
101/// This function drives the three server tasks in a `tokio::select!` loop and
102/// returns when any task errors or the future is dropped.
103pub async fn run_pva_server(
104    sources: Arc<SourceRegistry>,
105    config: PvaServerConfig,
106) -> Result<(), Box<dyn std::error::Error>> {
107    let registry = Arc::new(MonitorRegistry::new());
108    run_pva_server_with_registry(sources, config, registry).await
109}
110
111/// Like [`run_pva_server`] but re-uses an existing [`MonitorRegistry`].
112pub async fn run_pva_server_with_registry(
113    sources: Arc<SourceRegistry>,
114    config: PvaServerConfig,
115    registry: Arc<MonitorRegistry>,
116) -> Result<(), Box<dyn std::error::Error>> {
117    let server_state = PvaServerState::with_registry(sources, &config, registry);
118    let state = server_state.inner;
119
120    let guid = state.guid;
121    let tcp_addr = SocketAddr::new(config.listen_ip, config.tcp_port);
122    let udp_addr = SocketAddr::new(config.listen_ip, config.udp_port);
123
124    info!(
125        "Starting PVA server: udp={} tcp={} pvlist_mode={:?} pvlist_max={} filter={}",
126        udp_addr,
127        tcp_addr,
128        config.pvlist_mode,
129        config.pvlist_max,
130        config
131            .pvlist_allow_pattern
132            .as_ref()
133            .map(|r| r.as_str())
134            .unwrap_or("<none>")
135    );
136
137    // Bind TCP before spawning anything.  If the port is already in use we
138    // return an error here and the beacon is never started, preventing a
139    // ghost-beacon that advertises a port this process does not serve.
140    let tcp_listener = TcpListener::bind(tcp_addr).await?;
141    info!("TCP listener bound on {}", tcp_addr);
142
143    let beacon_config = BeaconConfig {
144        target: config.beacon_target,
145        guid,
146        tcp_port: config.tcp_port,
147        advertise_ip: config.advertise_ip,
148        listen_ip: config.listen_ip,
149        period_secs: config.beacon_period_secs,
150    };
151
152    let udp_state = state.clone();
153    let udp_task = tokio::spawn(async move {
154        if let Err(e) = run_udp_search(
155            udp_state,
156            udp_addr,
157            config.tcp_port,
158            guid,
159            config.advertise_ip,
160        )
161        .await
162        {
163            error!("UDP search server error: {}", e);
164        }
165    });
166
167    let tcp_state = state.clone();
168    let tcp_task = tokio::spawn(async move {
169        if let Err(e) = run_tcp_server(tcp_state, tcp_listener, config.conn_timeout).await {
170            error!("TCP server error: {}", e);
171        }
172    });
173
174    let beacon_change = state.beacon_change.clone();
175    let beacon_task = tokio::spawn(async move {
176        if let Err(e) = run_beacon(beacon_config, beacon_change).await {
177            error!("Beacon task error: {}", e);
178        }
179    });
180
181    let _ = tokio::join!(udp_task, tcp_task, beacon_task);
182    Ok(())
183}