use lazy_static::lazy_static;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
lazy_static! {
static ref INSTANCE_FILE: PathBuf = PathBuf::from("/etc/svgen/instances");
static ref TEMPLATE_DIR: PathBuf = PathBuf::from("/etc/svgen/templates/");
static ref GENERATED_DIR: PathBuf = PathBuf::from("/etc/sv/generated/");
static ref SERVICE_DIR: PathBuf = PathBuf::from("/service/");
static ref DEFAULT_LOG: PathBuf = PathBuf::from("/usr/local/bin/templog");
static ref SUPERVISE_TARGET: PathBuf = PathBuf::from("/run/runit/");
}
const SUPERVISE_PREFIX: &'static str = "supervise.";
static INSTANCE_STR: &'static str = "__INSTANCE__";
fn supervise_target_name(instance_name: &str) -> PathBuf {
SUPERVISE_TARGET.join([SUPERVISE_PREFIX, instance_name].join(""))
}
fn split_instance(instance: &str) -> Option<(&str, &str)> {
let mut sp = instance.splitn(2, '@');
match (sp.next(), sp.next()) {
(Some(a), Some(b)) => Some((a, b)),
_ => None,
}
}
fn copy_mode<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(src)?;
let mode = meta.permissions().mode();
let perm = Permissions::from_mode(mode);
std::fs::set_permissions(dst, perm)
}
fn process_file<P: AsRef<Path>, Q: AsRef<Path>>(
in_file: P,
out_file: Q,
instance_name: &str,
) -> Result<(), String> {
use std::fs::read_to_string;
use std::fs::write;
let in_file = in_file.as_ref();
let out_file = out_file.as_ref();
let data = read_to_string(in_file)
.map_err(|e| format!("Failed to read file '{}': {}", in_file.display(), e))?;
let data = data.replace(INSTANCE_STR, instance_name);
write(out_file, data)
.map_err(|e| format!("Failed to write file '{}': {}", out_file.display(), e))?;
copy_mode(in_file, out_file).map_err(|e| format!("Failed to copy access mode: {}", e))
}
fn gen_service(instance: &str) -> Result<(), String> {
use std::fs::create_dir_all;
use std::fs::read_dir;
use std::os::unix::fs::symlink;
let (template_name, instance_name) =
split_instance(instance).ok_or(format!("Invalid instance line: {}", instance))?;
let template = TEMPLATE_DIR.join(template_name);
let service_dir = GENERATED_DIR.join(instance);
let target_link = SERVICE_DIR.join(instance);
if service_dir.exists() || target_link.exists() {
println!(
"♻ Service '{}' already present, nothing to do.",
instance
);
return Ok(());
}
println!("➤ Generating service for instance {}", instance);
create_dir_all(&service_dir).unwrap();
let log_dir = service_dir.join("log");
create_dir_all(&log_dir).unwrap();
let runfile = template.join("run");
let logfile = template.join("log");
if !runfile.exists() {
return Err(format!("Template '{}' has no 'run' file", template_name));
}
let dst_runfile = service_dir.join("run");
let dst_logfile = log_dir.join("run");
process_file(runfile, &dst_runfile, instance_name)?;
if logfile.exists() {
process_file(logfile, &dst_logfile, instance_name)?;
} else {
symlink(DEFAULT_LOG.as_path(), dst_logfile).unwrap();
}
for file in read_dir(&template).unwrap() {
let file = file.unwrap();
let file_name = file.file_name();
if !file.file_type().unwrap().is_file()
|| file_name.as_bytes() == b"run"
|| file_name.as_bytes() == b"log"
{
continue;
}
let src = file.path();
let dst = service_dir.join(file.file_name());
process_file(src, dst, instance_name)?;
}
let supervise = supervise_target_name(instance_name);
symlink(supervise, service_dir.join("supervise")).unwrap();
symlink(service_dir, target_link).unwrap();
Ok(())
}
fn del_service(instance: &str) {
use std::fs::remove_dir_all;
use std::fs::remove_file;
use std::process::Command;
let service_dir = GENERATED_DIR.join(instance);
let target_link = SERVICE_DIR.join(instance);
if !Command::new("sv")
.args(&["down", instance])
.status()
.unwrap()
.success()
{
Command::new("sv")
.args(&["kill", instance])
.status()
.unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
}
remove_file(target_link).unwrap();
std::thread::sleep(std::time::Duration::from_secs(7));
remove_dir_all(service_dir).unwrap();
}
fn main() {
use std::fs::read_dir;
use std::fs::read_to_string;
let instances = read_to_string(INSTANCE_FILE.as_path()).unwrap();
let instances = instances
.split('\n')
.filter(|l| !(l.is_empty() || l.starts_with('#')))
.collect::<Vec<_>>();
for instance in &instances {
if let Err(e) = gen_service(instance) {
println!("⚠ {}", e);
continue;
}
}
for service_dir in read_dir(GENERATED_DIR.as_path()).unwrap() {
let service_dir = service_dir.unwrap();
let name = service_dir.file_name().into_string().unwrap();
if !instances.contains(&name.as_ref()) {
println!("🗑 Removing instance '{}'", name);
del_service(&name);
}
}
}