feat: add HTTP API and integration coverage
Expose the daemon request surface over /v1 with Axum, reuse shared key parsing between CLI and HTTP, and add an isolated end-to-end HTTP test that boots a real daemon process with temp XDG paths.
This commit is contained in:
+38
-4
@@ -6,6 +6,7 @@ pub mod registry;
|
||||
pub mod state;
|
||||
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -25,6 +26,7 @@ use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{UnixListener, UnixStream},
|
||||
sync::Mutex,
|
||||
task::JoinHandle,
|
||||
time::{self, MissedTickBehavior, sleep},
|
||||
};
|
||||
use tracing::warn;
|
||||
@@ -33,6 +35,9 @@ use tracing::warn;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::adapters::{Device, TvKey};
|
||||
use crate::api;
|
||||
|
||||
pub type SharedDaemon = Arc<Mutex<Daemon>>;
|
||||
|
||||
/// The long-lived tvctld process.
|
||||
#[derive(Debug)]
|
||||
@@ -85,7 +90,7 @@ impl Daemon {
|
||||
|
||||
/// Run the long-lived daemon loop over a Unix socket.
|
||||
pub async fn serve() -> anyhow::Result<()> {
|
||||
let daemon = Arc::new(Mutex::new(Daemon::load().await?));
|
||||
let daemon: SharedDaemon = Arc::new(Mutex::new(Daemon::load().await?));
|
||||
|
||||
{
|
||||
let mut guard = daemon.lock().await;
|
||||
@@ -101,6 +106,7 @@ pub async fn serve() -> anyhow::Result<()> {
|
||||
guard.config.discovery.interval_secs,
|
||||
)
|
||||
};
|
||||
let http_server = start_http_server_if_enabled(daemon.clone()).await?;
|
||||
|
||||
if let Some(parent) = socket_path.parent() {
|
||||
fs::create_dir_all(parent).await?;
|
||||
@@ -163,6 +169,9 @@ pub async fn serve() -> anyhow::Result<()> {
|
||||
let guard = daemon.lock().await;
|
||||
let _ = fs::remove_file(&guard.paths.active_socket_file).await;
|
||||
}
|
||||
if let Some(task) = http_server {
|
||||
task.abort();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -174,6 +183,31 @@ async fn set_socket_permissions(path: &Path) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_http_server_if_enabled(daemon: SharedDaemon) -> anyhow::Result<Option<JoinHandle<()>>> {
|
||||
let (enabled, host, port) = {
|
||||
let guard = daemon.lock().await;
|
||||
(
|
||||
guard.config.daemon.http_enabled,
|
||||
guard.config.daemon.http_host.clone(),
|
||||
guard.config.daemon.http_port,
|
||||
)
|
||||
};
|
||||
|
||||
if !enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let address: SocketAddr = format!("{host}:{port}").parse()?;
|
||||
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||
let app = api::router(daemon);
|
||||
let task = tokio::spawn(async move {
|
||||
if let Err(error) = axum::serve(listener, app).await {
|
||||
warn!("HTTP API server stopped: {error}");
|
||||
}
|
||||
});
|
||||
Ok(Some(task))
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
mut stream: UnixStream,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
@@ -194,7 +228,7 @@ async fn handle_connection(
|
||||
}
|
||||
};
|
||||
|
||||
let (response, should_stop) = handle_request(request, daemon).await;
|
||||
let (response, should_stop) = execute_request(request, daemon).await;
|
||||
write_response(&mut stream, &response).await?;
|
||||
Ok(should_stop)
|
||||
}
|
||||
@@ -206,9 +240,9 @@ async fn write_response(stream: &mut UnixStream, response: &DaemonResponse) -> a
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request(
|
||||
pub(crate) async fn execute_request(
|
||||
request: DaemonRequest,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
daemon: SharedDaemon,
|
||||
) -> (DaemonResponse, bool) {
|
||||
match request {
|
||||
DaemonRequest::Ping => {
|
||||
|
||||
Reference in New Issue
Block a user