use super::service_messages::{ClientFailure, Platform, render};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
pub const SOCKET_UNIT: &str = include_str!("service_units/cli-git.socket");
pub const SERVICE_UNIT: &str = include_str!("service_units/cli-git.service");
pub const SOCKET_UNIT_NAME: &str = "cli-git.socket";
pub const SERVICE_UNIT_NAME: &str = "cli-git.service";
pub const SERVICE_PROGRAM: &str = "cli-git-service";
const BINARY_PLACEHOLDER: &str = "@SERVICE_BINARY@";
const GIT_PLACEHOLDER: &str = "@GIT@";
#[derive(Clone, Debug)]
pub struct ServiceHost {
pub unit_directory: PathBuf,
pub systemctl: PathBuf,
pub service_binary: PathBuf,
pub git: PathBuf,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Report {
pub code: i32,
pub stdout: String,
pub stderr: String,
}
fn failed(failure: &ClientFailure) -> Report {
return Report { code: 1, stdout: String::new(), stderr: format!("{}\n", render(failure)) };
}
pub fn platform_without_manager() -> Platform {
if cfg!(windows) {
return Platform::Windows;
}
if cfg!(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly")) {
return Platform::Bsd;
}
if cfg!(target_os = "linux") {
return Platform::LinuxWithoutUserServices;
}
return Platform::Other;
}
pub fn unit_texts(host: &ServiceHost) -> [(&'static str, String); 2] {
let service: String = SERVICE_UNIT
.replace(BINARY_PLACEHOLDER, host.service_binary.to_string_lossy().as_ref())
.replace(GIT_PLACEHOLDER, host.git.to_string_lossy().as_ref());
return [(SOCKET_UNIT_NAME, SOCKET_UNIT.to_string()), (SERVICE_UNIT_NAME, service)];
}
fn systemctl(host: &ServiceHost, arguments: &[&str]) -> std::io::Result<Output> {
return Command::new(&host.systemctl).arg("--user").args(arguments).stdin(Stdio::null()).output();
}
fn manager_answers(host: &ServiceHost) -> bool {
return systemctl(host, &["show-environment"]).is_ok_and(|output| { return output.status.success(); });
}
fn command_text(output: &Output) -> String {
let text: String = String::from_utf8_lossy(output.stderr.as_slice()).trim().replace('\n', " ");
return if text.is_empty() { String::from("it gave no reason") } else { text };
}
pub fn install(host: &ServiceHost) -> Report {
if !host.service_binary.is_file() {
return failed(&ClientFailure::InstallProgramMissing(host.service_binary.display().to_string()));
}
if !manager_answers(host) {
return failed(&ClientFailure::NoActivation(platform_without_manager()));
}
if let Err(error) = std::fs::create_dir_all(&host.unit_directory) {
return failed(&ClientFailure::InstallCommandFailed(format!("{} cannot be created: {error}", host.unit_directory.display())));
}
let mut written: Vec<String> = Vec::new();
for (name, text) in unit_texts(host) {
let path: PathBuf = host.unit_directory.join(name);
if std::fs::read_to_string(&path).is_ok_and(|current| { return current == text; }) {
continue;
}
let temporary: PathBuf = host.unit_directory.join(format!("{name}.new"));
if let Err(error) = std::fs::write(&temporary, text.as_bytes()).and_then(|()| { return std::fs::rename(&temporary, &path); }) {
return failed(&ClientFailure::InstallCommandFailed(format!("{} cannot be written: {error}", path.display())));
}
written.push(path.display().to_string());
}
for arguments in [&["daemon-reload"][..], &["enable", "--now", SOCKET_UNIT_NAME][..]] {
match systemctl(host, arguments) {
Ok(output) if output.status.success() => {}
Ok(output) => return failed(&ClientFailure::InstallCommandFailed(command_text(&output))),
Err(error) => return failed(&ClientFailure::InstallCommandFailed(error.to_string())),
}
}
let mut lines: Vec<String> = Vec::new();
if written.is_empty() {
lines.push(String::from("The commit service was already installed for this account; nothing changed."));
} else {
lines.push(String::from("Installed the commit service for this account."));
for path in written {
lines.push(format!(" Wrote `{path}`."));
}
}
lines.push(format!(" Enabled `{SOCKET_UNIT_NAME}`; the service itself starts when the first commit asks for it."));
lines.push(String::from(" Plain scoped commits go through it from now on; `CLI_GIT_COMMIT_SERVICE=0` keeps one commit out of it."));
return Report { code: 0, stdout: format!("{}\n", lines.join("\n")), stderr: String::new() };
}
pub fn uninstall(host: &ServiceHost) -> Report {
if !manager_answers(host) {
return failed(&ClientFailure::NoActivation(platform_without_manager()));
}
let _disabled = systemctl(host, &["disable", "--now", SOCKET_UNIT_NAME, SERVICE_UNIT_NAME]);
let mut removed: Vec<String> = Vec::new();
for name in [SOCKET_UNIT_NAME, SERVICE_UNIT_NAME] {
let path: PathBuf = host.unit_directory.join(name);
if path.exists() {
if let Err(error) = std::fs::remove_file(&path) {
return failed(&ClientFailure::InstallCommandFailed(format!("{} cannot be removed: {error}", path.display())));
}
removed.push(path.display().to_string());
}
}
let _reloaded = systemctl(host, &["daemon-reload"]);
let text: String = if removed.is_empty() {
String::from("The commit service was not installed for this account; nothing changed.\n")
} else {
format!("Removed the commit service for this account.\n{}\n", removed.iter().map(|path| { return format!(" Removed `{path}`."); }).collect::<Vec<String>>().join("\n"))
};
return Report { code: 0, stdout: text, stderr: String::new() };
}
pub fn status(host: &ServiceHost) -> Report {
if !manager_answers(host) {
return failed(&ClientFailure::NoActivation(platform_without_manager()));
}
let listening: bool = systemctl(host, &["is-active", SOCKET_UNIT_NAME]).is_ok_and(|output| { return output.status.success(); });
return if listening {
Report { code: 0, stdout: String::from("The commit service is installed and ready for commits.\n"), stderr: String::new() }
} else {
Report { code: 1, stdout: String::new(), stderr: format!("{}\n", render(&ClientFailure::NotInstalled)) }
};
}
pub fn unit_directory(config_home: Option<&Path>, home: Option<&Path>) -> Option<PathBuf> {
return config_home
.map(|directory| { return directory.join("systemd").join("user"); })
.or_else(|| { return home.map(|directory| { return directory.join(".config").join("systemd").join("user"); }); });
}
#[cfg(test)]
#[path = "service_install_tests.rs"]
mod tests;