Split code in modules.
This commit is contained in:
parent
c7a481976d
commit
1620a81d15
7 changed files with 277 additions and 244 deletions
78
src/activate.rs
Normal file
78
src/activate.rs
Normal 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
109
src/generate.rs
Normal 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
46
src/lib.rs
Normal 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))
|
||||
}
|
||||
226
src/main.rs
226
src/main.rs
|
|
@ -1,35 +1,7 @@
|
|||
mod systemd;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
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";
|
||||
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)
|
||||
}
|
||||
}
|
||||
use service_manager::StorePath;
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
#[command(author, version, about, long_about=None)]
|
||||
|
|
@ -58,26 +30,9 @@ fn main() {
|
|||
|
||||
match args.action {
|
||||
Action::Activate { store_path } => handle_toplevel_error(activate(store_path)),
|
||||
Action::Generate { flake_uri } => handle_toplevel_error(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())
|
||||
Action::Generate { flake_uri } => {
|
||||
handle_toplevel_error(service_manager::generate::generate(&flake_uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,176 +40,11 @@ fn activate(store_path: StorePath) -> Result<()> {
|
|||
if !nix::unistd::Uid::is_root(nix::unistd::getuid()) {
|
||||
return Err(anyhow!("We need root permissions."));
|
||||
}
|
||||
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(())
|
||||
service_manager::activate::activate(store_path)
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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."))
|
||||
})
|
||||
fn handle_toplevel_error<T>(r: Result<T>) {
|
||||
if let Err(e) = r {
|
||||
log::error!("{}", e)
|
||||
}
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,8 +155,8 @@ impl ServiceManager {
|
|||
let job_names_clone = Arc::clone(&job_names);
|
||||
let token = self.proxy.match_signal(
|
||||
move |h: OrgFreedesktopSystemd1ManagerJobRemoved, _: &Connection, _: &Message| {
|
||||
log::debug!("{} added", h.unit);
|
||||
log_thread("Signal handling");
|
||||
log::info!("Job for {} done", h.unit);
|
||||
{
|
||||
// Insert a new name, and let the lock go out of scope immediately
|
||||
job_names_clone.lock().unwrap().insert(h.unit);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue