dynix/src/daemon.rs

232 lines
6.7 KiB
Rust
Raw Normal View History

2026-03-11 14:26:59 +01:00
use std::{
net::SocketAddr,
2026-03-30 13:39:56 +02:00
process::{Output, Stdio},
sync::LazyLock,
2026-03-11 14:26:59 +01:00
};
2026-03-30 13:39:56 +02:00
use axum::{
Json, Router,
extract::State,
http::{self, StatusCode, header::HeaderMap},
routing::post,
};
2026-03-30 13:39:56 +02:00
use tokio::{net::TcpListener, process::Command};
//use utoipa::{OpenApi as _, ToSchema, openapi::OpenApi};
//use utoipa_axum::router::{OpenApiRouter, UtoipaMethodRouterExt};
2026-03-11 14:26:59 +01:00
2026-03-30 13:39:56 +02:00
use serde::{Deserialize, Serialize};
2026-03-22 17:15:04 +01:00
2026-03-30 13:39:56 +02:00
use crate::{SourceFile, prelude::*};
pub mod api;
2026-03-30 13:39:56 +02:00
use api::{ConvenientAttrPath, NixLiteral};
2026-03-30 13:39:56 +02:00
//pub static UID: LazyLock<Uid> = LazyLock::new(rustix::process::getuid);
2026-03-11 14:26:59 +01:00
2026-03-30 13:39:56 +02:00
//pub static USER_SOCKET_DIR: LazyLock<&'static Path> = LazyLock::new(|| {
// let dir: Box<Path> = env::var_os("XDG_RUNTIME_DIR")
// .map(PathBuf::from)
// .unwrap_or_else(|| ["/", "run", "user", &UID.to_string()].into_iter().collect())
// .into_boxed_path();
//
// Box::leak(dir)
//});
2026-03-11 14:26:59 +01:00
2026-03-30 13:39:56 +02:00
//pub static TMPDIR: LazyLock<&'static Path> = LazyLock::new(|| {
// let dir: Box<Path> = env::temp_dir().into_boxed_path();
//
// Box::leak(dir)
//});
2026-03-19 12:36:46 +01:00
2026-03-22 17:15:04 +01:00
pub static NIX: LazyLock<&'static Path> = LazyLock::new(|| {
which::which("nix")
.inspect_err(|e| error!("couldn't find `nix` in PATH: {e}"))
.map(PathBuf::into_boxed_path)
.map(|boxed| &*Box::leak(boxed))
.unwrap_or(Path::new("/run/current-system/sw/bin/nix"))
});
2026-03-30 13:39:56 +02:00
pub async fn run(config: Config) {
let addr = config.addr.clone();
let router = Router::new()
.route("/set", post(ep_set_post))
// `.with_state()` has to be last for the type inference to work.
.with_state(config);
//let (router, api): (Router, OpenApi) = OpenApiRouter::with_openapi(ApiDoc::openapi())
// .routes(utoipa_axum::routes!(ep_set_post))
// // `.with_state()` has to be last for the type inference works.
// .with_state(config)
// .split_for_parts();
2026-03-11 14:26:59 +01:00
2026-03-30 13:39:56 +02:00
let listener = TcpListener::bind(addr).await.unwrap();
2026-03-30 13:39:56 +02:00
axum::serve(listener, router).await.unwrap();
}
2026-03-30 13:39:56 +02:00
#[derive(Debug, Clone)]
pub struct Config {
pub config_file: SourceFile,
pub addr: SocketAddr,
pub token: Option<String>,
2026-03-11 14:26:59 +01:00
}
2026-03-30 13:39:56 +02:00
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[derive(Deserialize, Serialize)]
//#[derive(ToSchema)]
pub struct SetParams {
pub name: ConvenientAttrPath,
pub value: NixLiteral,
2026-03-19 19:45:09 +01:00
}
2026-03-30 13:39:56 +02:00
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Deserialize, Serialize)]
//#[derive(ToSchema)]
pub struct SetResponse {
/// Will be 0 if everything is okay.
2026-03-11 14:26:59 +01:00
///
2026-03-30 13:39:56 +02:00
/// Will be -1 for an error with no code.
pub status: i64,
pub msg: Option<String>,
2026-03-11 14:26:59 +01:00
}
2026-03-30 13:39:56 +02:00
#[axum::debug_handler]
//#[utoipa::path(
// post,
// path = "/set",
// responses(
// (status = 200, description = "Request was valid", body = SetResponse)
// ),
//)]
async fn ep_set_post(
State(config): State<Config>,
headers: HeaderMap,
Json(SetParams { name, value }): Json<SetParams>,
) -> Result<Json<SetResponse>, StatusCode> {
debug!("POST /set with name={name:?}, value={value:?}");
if let Some(token) = &config.token {
let Some(auth) = headers.get(http::header::AUTHORIZATION) else {
// FIXME: technically RFC9110 requires us to respond with a
// `WWW-Authenticate` header.
error!("token specified in config but not provided in request");
return Err(StatusCode::UNAUTHORIZED);
2026-03-22 17:15:04 +01:00
};
2026-03-30 13:39:56 +02:00
// No need to go through UTF-8 decoding here.
if auth.as_bytes() != token.as_bytes() {
error!("token provided in request does not match configured token");
return Err(StatusCode::UNAUTHORIZED);
2026-03-11 14:26:59 +01:00
}
}
2026-03-30 13:39:56 +02:00
let file = config.config_file.clone();
2026-03-30 13:39:56 +02:00
let prio = crate::get_where(file.clone());
let new_prio = prio - 1;
2026-03-30 13:39:56 +02:00
let opt_name = name.to_nix_decl();
let opt_val = value.to_nix_source();
let new_line = crate::get_next_prio_line(file.clone(), &opt_name, new_prio, &opt_val);
2026-03-30 13:39:56 +02:00
match crate::write_next_prio(file.clone(), new_line) {
Ok(()) => (),
Err(e) => {
error!("Couldn't write next generation to {}: {e}", file.display());
let status = e.raw_os_error().map(i64::from).unwrap_or(-1);
return Ok(Json(SetResponse {
status,
msg: Some(format!("{e}")),
}));
},
};
let child_status = match nix_run_apply(&config).await {
Ok(v) => v,
Err(e) => {
let status = e.raw_os_error().map(i64::from).unwrap_or(-1);
return Ok(Json(SetResponse {
status,
msg: Some(format!("{e}")),
}));
},
};
let Output {
status,
stdout,
stderr,
} = child_status;
if status.code() != Some(0) {
error!(
"Child `nix run` process returned non-zero code {:?}",
status.code(),
);
error!("Child stdout: {}", stdout.as_bstr());
error!("Child stderr: {}", stderr.as_bstr());
}
2026-03-30 13:39:56 +02:00
let status = status.code().map(i64::from).unwrap_or(-1);
let msg = format!(
"Stdout: {}\nStderr: {}\n",
stdout.as_bstr(),
stderr.as_bstr()
);
2026-03-30 13:39:56 +02:00
Ok(Json(SetResponse {
status,
msg: Some(msg),
}))
2026-03-19 12:36:46 +01:00
}
2026-03-30 13:39:56 +02:00
async fn nix_run_apply(config: &Config) -> Result<Output, IoError> {
let configuration_nix = config
.config_file
.path()
.parent()
.unwrap()
.join("configuration.nix");
let configuration_nix = configuration_nix
.to_str()
.expect("specified NixOS config file is not a UTF-8 path");
let expr = format!(
"(import <nixpkgs/nixos> {{ configuration = {}; }})\
.config.dynamicism.applyDynamicConfiguration {{ baseConfiguration = {}; }}",
configuration_nix, configuration_nix,
);
let child = Command::new(*NIX)
.arg("run")
.arg("--show-trace")
.arg("--log-format")
.arg("raw-with-logs")
.arg("--impure")
.arg("-E")
.arg(expr)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.tap(|cmd| {
if tracing::enabled!(Level::DEBUG) {
let args = cmd
.as_std()
.get_args()
.map(OsStr::to_string_lossy)
.join(" ");
debug!("Spawning command: `nix {args}`");
}
})
.spawn()
.inspect_err(|e| error!("error spawning command: {e}"))?;
let output = child.wait_with_output().await.inspect_err(|e| {
error!("couldn't wait for spawned child process: {e}");
})?;
Ok(output)
2026-03-11 14:26:59 +01:00
}
2026-03-30 13:39:56 +02:00
//#[derive(Copy)]
//#[derive(Debug, Clone, PartialEq)]
//#[derive(utoipa::OpenApi)]
//#[openapi(paths(ep_set_post))]
//pub struct ApiDoc;