feat: finalize HTTP direct dispatch refactor and pending milestone updates
Switch API execution to direct daemon request handling, add regression coverage for non-socket HTTP dispatch, and include the remaining pending local updates across CLI/daemon/docs from the current worktree.
This commit is contained in:
+26
-1
@@ -7,6 +7,7 @@ use std::{
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
/// The complete daemon configuration loaded from TOML.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
@@ -93,7 +94,10 @@ impl TvctlConfig {
|
||||
"daemon.http_enabled" => self.daemon.http_enabled = parse_bool(key, value)?,
|
||||
"daemon.http_port" => self.daemon.http_port = parse_value(key, value)?,
|
||||
"daemon.http_host" => self.daemon.http_host = value.to_string(),
|
||||
"daemon.log_level" => self.daemon.log_level = value.to_string(),
|
||||
"daemon.log_level" => {
|
||||
validate_log_level(value)?;
|
||||
self.daemon.log_level = value.to_string();
|
||||
}
|
||||
"discovery.auto_discover" => self.discovery.auto_discover = parse_bool(key, value)?,
|
||||
"discovery.interval_secs" => self.discovery.interval_secs = parse_value(key, value)?,
|
||||
"discovery.timeout_secs" => self.discovery.timeout_secs = parse_value(key, value)?,
|
||||
@@ -311,6 +315,13 @@ where
|
||||
.map_err(|error| anyhow::anyhow!("invalid value '{value}' for {key}: {error}"))
|
||||
}
|
||||
|
||||
fn validate_log_level(value: &str) -> anyhow::Result<()> {
|
||||
EnvFilter::try_new(value).map_err(|error| {
|
||||
anyhow::anyhow!("invalid value '{value}' for daemon.log_level: {error}")
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -348,4 +359,18 @@ mod tests {
|
||||
.expect("config should load");
|
||||
assert_eq!(loaded, config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_log_level_values() {
|
||||
let mut config = TvctlConfig::default();
|
||||
let error = config
|
||||
.set_value("daemon.log_level", "[")
|
||||
.expect_err("invalid log level should fail");
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("invalid value '[' for daemon.log_level"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -36,6 +36,7 @@ use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::adapters::{Device, TvKey};
|
||||
use crate::api;
|
||||
use crate::logging;
|
||||
|
||||
pub type SharedDaemon = Arc<Mutex<Daemon>>;
|
||||
|
||||
@@ -62,6 +63,7 @@ impl Daemon {
|
||||
/// Load the daemon's persisted state and adapter registry.
|
||||
pub async fn load() -> anyhow::Result<Self> {
|
||||
let config = TvctlConfig::load().await?;
|
||||
logging::apply_log_level(&config.daemon.log_level).map_err(anyhow::Error::msg)?;
|
||||
let mut paths = RuntimePaths::detect();
|
||||
if !config.daemon.socket.is_empty() {
|
||||
paths.socket_file = PathBuf::from(&config.daemon.socket);
|
||||
@@ -783,16 +785,26 @@ pub(crate) async fn execute_request(
|
||||
DaemonRequest::ReloadConfig => {
|
||||
let mut guard = daemon.lock().await;
|
||||
match TvctlConfig::load_from_path(&guard.paths.config_file).await {
|
||||
Ok(config) => {
|
||||
let restart_required = apply_runtime_config(&mut guard, config);
|
||||
(
|
||||
Ok(config) => match apply_runtime_config(&mut guard, config) {
|
||||
Ok(restart_required) => (
|
||||
DaemonResponse::success(ConfigReloadResult {
|
||||
config: guard.config.clone(),
|
||||
restart_required,
|
||||
}),
|
||||
false,
|
||||
)
|
||||
}
|
||||
),
|
||||
Err(error) => (
|
||||
DaemonResponse::error(
|
||||
"config_reload_failed",
|
||||
format!("Failed to apply reloaded config: {error}"),
|
||||
Some(
|
||||
"Inspect ~/.config/tvctl/config.toml and fix invalid values."
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
false,
|
||||
),
|
||||
},
|
||||
Err(error) => (
|
||||
DaemonResponse::error(
|
||||
"config_reload_failed",
|
||||
@@ -943,8 +955,9 @@ fn discovery_interval(interval_secs: u64) -> Option<time::Interval> {
|
||||
Some(interval)
|
||||
}
|
||||
|
||||
fn apply_runtime_config(daemon: &mut Daemon, config: TvctlConfig) -> Vec<String> {
|
||||
fn apply_runtime_config(daemon: &mut Daemon, config: TvctlConfig) -> anyhow::Result<Vec<String>> {
|
||||
let old_config = daemon.config.clone();
|
||||
logging::apply_log_level(&config.daemon.log_level).map_err(anyhow::Error::msg)?;
|
||||
daemon.adapters = AdapterRegistry::from_config(&config);
|
||||
daemon.discovery = DiscoveryService::new(daemon.adapters.clone());
|
||||
|
||||
@@ -972,5 +985,5 @@ fn apply_runtime_config(daemon: &mut Daemon, config: TvctlConfig) -> Vec<String>
|
||||
}
|
||||
|
||||
daemon.config = config;
|
||||
restart_required
|
||||
Ok(restart_required)
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ impl AdapterRegistry {
|
||||
F: for<'a> FnOnce(
|
||||
&'a RokuAdapter,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = crate::adapters::Result<T>> + 'a>,
|
||||
Box<dyn std::future::Future<Output = crate::adapters::Result<T>> + Send + 'a>,
|
||||
>,
|
||||
{
|
||||
match platform {
|
||||
@@ -309,7 +309,7 @@ impl AdapterRegistry {
|
||||
&'a RokuAdapter,
|
||||
&'a Device,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = crate::adapters::Result<T>> + 'a>,
|
||||
Box<dyn std::future::Future<Output = crate::adapters::Result<T>> + Send + 'a>,
|
||||
>,
|
||||
{
|
||||
match device.platform.as_str() {
|
||||
|
||||
Reference in New Issue
Block a user