2026-03-11 14:26:59 +01:00
|
|
|
use std::{
|
2026-03-26 12:44:48 +01:00
|
|
|
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-26 12:44:48 +01:00
|
|
|
};
|
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::*};
|
2026-03-22 17:15:04 +01:00
|
|
|
|
|
|
|
|
pub mod api;
|
2026-03-30 13:39:56 +02:00
|
|
|
use api::{ConvenientAttrPath, NixLiteral};
|
2026-03-22 17:15:04 +01:00
|
|
|
|
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-19 20:44:57 +01:00
|
|
|
|
2026-03-30 13:39:56 +02:00
|
|
|
axum::serve(listener, router).await.unwrap();
|
2026-03-19 20:44:57 +01:00
|
|
|
}
|
|
|
|
|
|
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-22 17:15:04 +01:00
|
|
|
|
2026-03-30 13:39:56 +02:00
|
|
|
let file = config.config_file.clone();
|
2026-03-22 17:15:04 +01:00
|
|
|
|
2026-03-30 13:39:56 +02:00
|
|
|
let prio = crate::get_where(file.clone());
|
|
|
|
|
let new_prio = prio - 1;
|
2026-03-22 17:15:04 +01:00
|
|
|
|
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-22 17:15:04 +01:00
|
|
|
|
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-22 17:15:04 +01:00
|
|
|
}
|
|
|
|
|
|
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-22 17:15:04 +01:00
|
|
|
|
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;
|