Split code in modules.

This commit is contained in:
R-VdP 2023-02-08 14:10:16 +00:00
parent c7a481976d
commit 1620a81d15
No known key found for this signature in database
7 changed files with 277 additions and 244 deletions

54
Cargo.lock generated
View file

@ -13,9 +13,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.68" version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -125,18 +125,15 @@ dependencies = [
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.2.6" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" checksum = "856b5cb0902c2b6d65d5fd97dfa30f9b70c7538e770b98eab5ed52d8db923e01"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "humantime" name = "humantime"
@ -146,9 +143,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.4" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys",
@ -156,9 +153,9 @@ dependencies = [
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.2" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"io-lifetimes", "io-lifetimes",
@ -180,9 +177,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]] [[package]]
name = "libdbus-sys" name = "libdbus-sys"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2264f9d90a9b4e60a2dc722ad899ea0374f03c2e96e755fe22a8f551d4d5fb3c" checksum = "9f8d7ae751e1cb825c840ae5e682f59b098cdfd213c350ac268b61449a5f58a0"
dependencies = [ dependencies = [
"pkg-config", "pkg-config",
] ]
@ -281,9 +278,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.50" version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -316,9 +313,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.36.7" version = "0.36.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -356,9 +353,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.91" version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -366,7 +363,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "service-manager" name = "service_manager"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@ -456,9 +453,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.42.0" version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm",
"windows_aarch64_msvc", "windows_aarch64_msvc",

View file

@ -1,10 +1,14 @@
[package] [package]
name = "service-manager" name = "service_manager"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "service-manager"
path = "src/main.rs"
[dependencies] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
clap = { version = "4.1.4", features = ["derive"] } clap = { version = "4.1.4", features = ["derive"] }

78
src/activate.rs Normal file
View file

@ -0,0 +1,78 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
use std::time::Duration;
use std::{fs, io, iter, str};
use super::{create_store_link, systemd, StorePath};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceConfig {
name: String,
service: String,
}
impl ServiceConfig {
fn store_path(&self) -> StorePath {
StorePath::from(self.service.to_owned())
}
}
pub fn activate(store_path: StorePath) -> Result<()> {
log::info!("Activating service-manager profile: {}", store_path);
let file = fs::File::open(store_path.path + "/services/services.json")?;
let reader = io::BufReader::new(file);
let services: Vec<ServiceConfig> = serde_json::from_reader(reader)?;
services.iter().try_for_each(|service| {
create_store_link(
&service.store_path(),
Path::new(&format!("/run/systemd/system/{}", service.name)),
)
})?;
let service_manager = systemd::ServiceManager::new_session()?;
start_services(&service_manager, &services, &Some(Duration::from_secs(30)))?;
Ok(())
}
fn start_services(
service_manager: &systemd::ServiceManager,
services: &[ServiceConfig],
timeout: &Option<Duration>,
) -> Result<()> {
service_manager.daemon_reload()?;
let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.iter().fold(HashSet::new(), |set, service| {
match service_manager.restart_unit(&service.name) {
Ok(_) => {
log::info!("Restarting service {}...", service.name);
set.into_iter()
.chain(iter::once(Box::new(service.name.to_owned())))
.collect()
}
Err(e) => {
log::error!(
"Error restarting unit, please consult the logs: {}",
service.name
);
log::error!("{}", e);
set
}
}
});
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs");
}
// TODO: do we want to propagate unit failures here in some way?
Ok(())
}

109
src/generate.rs Normal file
View file

@ -0,0 +1,109 @@
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::{env, fs, process, str};
use super::{create_store_link, StorePath, FLAKE_ATTR, PROFILE_NAME};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NixBuildOutput {
drv_path: String,
outputs: HashMap<String, String>,
}
pub fn generate(flake_uri: &str) -> Result<()> {
let user = env::var("USER")?;
// TODO: we temporarily put this under per-user to avoid needing root access
// we will move this to /nix/var/nix/profiles/ later on.
let profiles_dir = format!("profiles/per-user/{}", user);
let gcroots_dir = format!("gcroots/per-user/{}", user);
let profile_path = format!("/nix/var/nix/{}/{}", profiles_dir, PROFILE_NAME);
let gcroot_path = format!("/nix/var/nix/{}/{}-current", gcroots_dir, PROFILE_NAME);
// FIXME: we should not hard-code the system here
let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR);
log::info!("Running nix build...");
let store_path = run_nix_build(flake_uri, &flake_attr).and_then(get_store_path)?;
log::info!("Generating new generation from {}", store_path);
install_nix_profile(&store_path, &profile_path).map(print_out_and_err)?;
log::info!("Registering GC root...");
create_gcroot(&gcroot_path, &profile_path)?;
Ok(())
}
fn install_nix_profile(store_path: &StorePath, profile_path: &str) -> Result<process::Output> {
process::Command::new("nix-env")
.arg("--profile")
.arg(profile_path)
.arg("--set")
.arg(&store_path.path)
.output()
.map_err(anyhow::Error::from)
}
fn create_gcroot(gcroot_path: &str, profile_path: &str) -> Result<()> {
let profile_store_path = fs::canonicalize(profile_path)?;
let store_path = StorePath::from(String::from(profile_store_path.to_string_lossy()));
create_store_link(&store_path, Path::new(gcroot_path))
}
fn get_store_path(nix_build_result: process::Output) -> Result<StorePath> {
if nix_build_result.status.success() {
String::from_utf8(nix_build_result.stdout)
.map_err(anyhow::Error::from)
.and_then(parse_nix_build_output)
} else {
String::from_utf8(nix_build_result.stderr)
.map_err(anyhow::Error::from)
.and_then(|e| {
log::error!("{}", e);
Err(anyhow!("Nix build failed."))
})
}
}
fn parse_nix_build_output(output: String) -> Result<StorePath> {
let expected_output_name = "out";
let results: Vec<NixBuildOutput> = serde_json::from_str(&output)?;
if let [result] = results.as_slice() {
if let Some(store_path) = result.outputs.get(expected_output_name) {
return Ok(StorePath::from(store_path.to_owned()));
}
return Err(anyhow!(
"No output '{}' found in nix build result.",
expected_output_name
));
}
Err(anyhow!(
"Multiple build results were returned, we cannot handle that yet."
))
}
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> {
process::Command::new("nix")
.arg("build")
.arg(format!("{}#{}", flake_uri, flake_attr))
.arg("--json")
.output()
.map_err(anyhow::Error::from)
}
fn print_out_and_err(output: process::Output) -> process::Output {
print_u8(&output.stdout);
print_u8(&output.stderr);
output
}
fn print_u8(bytes: &[u8]) {
str::from_utf8(bytes).map_or((), |s| {
if !s.trim().is_empty() {
log::info!("{}", s.trim())
}
})
}

46
src/lib.rs Normal file
View file

@ -0,0 +1,46 @@
pub mod activate;
pub mod generate;
mod systemd;
use anyhow::Result;
use std::os::unix;
use std::path::Path;
use std::{fs, str};
const FLAKE_ATTR: &str = "serviceConfig";
const PROFILE_NAME: &str = "service-manager";
#[derive(Debug, Clone)]
pub struct StorePath {
pub path: String,
}
impl From<String> for StorePath {
fn from(path: String) -> Self {
StorePath {
path: path.trim().into(),
}
}
}
impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)
}
}
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
log::info!("Creating symlink: {} -> {}", from.display(), store_path);
if from.is_symlink() {
fs::remove_file(from)?;
}
unix::fs::symlink(&store_path.path, from).map_err(anyhow::Error::from)
}
pub fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(B) -> C,
G: Fn(A) -> B,
{
move |x| f(g(x))
}

View file

@ -1,35 +1,7 @@
mod systemd;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use clap::Parser; use clap::Parser;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::os::unix;
use std::path::Path;
use std::time::Duration;
use std::{env, fs, io, iter, process, str};
const FLAKE_ATTR: &str = "serviceConfig"; use service_manager::StorePath;
const PROFILE_NAME: &str = "service-manager";
#[derive(Debug, Clone)]
struct StorePath {
path: String,
}
impl From<String> for StorePath {
fn from(path: String) -> Self {
StorePath {
path: path.trim().into(),
}
}
}
impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)
}
}
#[derive(clap::Parser, Debug)] #[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about=None)] #[command(author, version, about, long_about=None)]
@ -58,26 +30,9 @@ fn main() {
match args.action { match args.action {
Action::Activate { store_path } => handle_toplevel_error(activate(store_path)), Action::Activate { store_path } => handle_toplevel_error(activate(store_path)),
Action::Generate { flake_uri } => handle_toplevel_error(generate(&flake_uri)), Action::Generate { flake_uri } => {
} handle_toplevel_error(service_manager::generate::generate(&flake_uri))
} }
fn handle_toplevel_error<T>(r: Result<T>) {
if let Err(e) = r {
log::error!("{}", e)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceConfig {
name: String,
service: String,
}
impl ServiceConfig {
fn store_path(&self) -> StorePath {
StorePath::from(self.service.to_owned())
} }
} }
@ -85,176 +40,11 @@ fn activate(store_path: StorePath) -> Result<()> {
if !nix::unistd::Uid::is_root(nix::unistd::getuid()) { if !nix::unistd::Uid::is_root(nix::unistd::getuid()) {
return Err(anyhow!("We need root permissions.")); return Err(anyhow!("We need root permissions."));
} }
log::info!("Activating service-manager profile: {}", store_path); service_manager::activate::activate(store_path)
let file = fs::File::open(store_path.path + "/services/services.json")?;
let reader = io::BufReader::new(file);
let services: Vec<ServiceConfig> = serde_json::from_reader(reader)?;
services.iter().try_for_each(|service| {
create_store_link(
&service.store_path(),
Path::new(&format!("/run/systemd/system/{}", service.name)),
)
})?;
let service_manager = systemd::ServiceManager::new_session()?;
start_services(&service_manager, &services, &Some(Duration::from_secs(30)))?;
Ok(())
} }
fn start_services( fn handle_toplevel_error<T>(r: Result<T>) {
service_manager: &systemd::ServiceManager, if let Err(e) = r {
services: &[ServiceConfig], log::error!("{}", e)
timeout: &Option<Duration>,
) -> Result<()> {
service_manager.daemon_reload()?;
let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.iter().fold(HashSet::new(), |set, service| {
match service_manager.restart_unit(&service.name) {
Ok(_) => {
log::info!("Restarting service {}...", service.name);
set.into_iter()
.chain(iter::once(Box::new(service.name.to_owned())))
.collect()
}
Err(e) => {
log::error!(
"Error restarting unit, please consult the logs: {}",
service.name
);
log::error!("{}", e);
set
}
}
});
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs");
}
// TODO: do we want to propagate unit failures here in some way?
Ok(())
}
fn generate(flake_uri: &str) -> Result<()> {
let user = env::var("USER")?;
// TODO: we temporarily put this under per-user to avoid needing root access
// we will move this to /nix/var/nix/profiles/ later on.
let profiles_dir = format!("profiles/per-user/{}", user);
let gcroots_dir = format!("gcroots/per-user/{}", user);
let profile_path = format!("/nix/var/nix/{}/{}", profiles_dir, PROFILE_NAME);
let gcroot_path = format!("/nix/var/nix/{}/{}-current", gcroots_dir, PROFILE_NAME);
// FIXME: we should not hard-code the system here
let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR);
log::info!("Running nix build...");
let store_path = run_nix_build(flake_uri, &flake_attr).and_then(get_store_path)?;
log::info!("Generating new generation from {}", store_path);
install_nix_profile(&store_path, &profile_path).map(print_out_and_err)?;
log::info!("Registering GC root...");
create_gcroot(&gcroot_path, &profile_path)?;
Ok(())
}
fn install_nix_profile(store_path: &StorePath, profile_path: &str) -> Result<process::Output> {
process::Command::new("nix-env")
.arg("--profile")
.arg(profile_path)
.arg("--set")
.arg(&store_path.path)
.output()
.map_err(anyhow::Error::from)
}
fn create_gcroot(gcroot_path: &str, profile_path: &str) -> Result<()> {
let profile_store_path = fs::canonicalize(profile_path)?;
let store_path = StorePath::from(String::from(profile_store_path.to_string_lossy()));
create_store_link(&store_path, Path::new(gcroot_path))
}
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
log::info!("Creating symlink: {} -> {}", from.display(), store_path);
if from.is_symlink() {
fs::remove_file(from)?;
}
unix::fs::symlink(&store_path.path, from).map_err(anyhow::Error::from)
}
fn get_store_path(nix_build_result: process::Output) -> Result<StorePath> {
if nix_build_result.status.success() {
String::from_utf8(nix_build_result.stdout)
.map_err(anyhow::Error::from)
.and_then(parse_nix_build_output)
} else {
String::from_utf8(nix_build_result.stderr)
.map_err(anyhow::Error::from)
.and_then(|e| {
log::error!("{}", e);
Err(anyhow!("Nix build failed."))
})
} }
} }
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NixBuildOutput {
drv_path: String,
outputs: HashMap<String, String>,
}
fn parse_nix_build_output(output: String) -> Result<StorePath> {
let expected_output_name = "out";
let results: Vec<NixBuildOutput> = serde_json::from_str(&output)?;
if let [result] = results.as_slice() {
if let Some(store_path) = result.outputs.get(expected_output_name) {
return Ok(StorePath::from(store_path.to_owned()));
}
return Err(anyhow!(
"No output '{}' found in nix build result.",
expected_output_name
));
}
Err(anyhow!(
"Multiple build results were returned, we cannot handle that yet."
))
}
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> {
process::Command::new("nix")
.arg("build")
.arg(format!("{}#{}", flake_uri, flake_attr))
.arg("--json")
.output()
.map_err(anyhow::Error::from)
}
fn print_out_and_err(output: process::Output) -> process::Output {
print_u8(&output.stdout);
print_u8(&output.stderr);
output
}
fn print_u8(bytes: &[u8]) {
str::from_utf8(bytes).map_or((), |s| {
if !s.trim().is_empty() {
log::info!("{}", s.trim())
}
})
}
pub fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(B) -> C,
G: Fn(A) -> B,
{
move |x| f(g(x))
}

View file

@ -155,8 +155,8 @@ impl ServiceManager {
let job_names_clone = Arc::clone(&job_names); let job_names_clone = Arc::clone(&job_names);
let token = self.proxy.match_signal( let token = self.proxy.match_signal(
move |h: OrgFreedesktopSystemd1ManagerJobRemoved, _: &Connection, _: &Message| { move |h: OrgFreedesktopSystemd1ManagerJobRemoved, _: &Connection, _: &Message| {
log::debug!("{} added", h.unit);
log_thread("Signal handling"); log_thread("Signal handling");
log::info!("Job for {} done", h.unit);
{ {
// Insert a new name, and let the lock go out of scope immediately // Insert a new name, and let the lock go out of scope immediately
job_names_clone.lock().unwrap().insert(h.unit); job_names_clone.lock().unwrap().insert(h.unit);