use running_process::broker::builders::ServiceDefinitionBuilder;
use running_process::broker::protocol::ServiceDefinition;
use running_process::broker::protocol_v2;
use running_process::broker::server::service_definition_dir;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub(crate) const ZCCACHE_SERVICE_NAME: &str = "zccache";
#[derive(Debug, Clone)]
pub(crate) struct InstalledServiceDefinition {
pub(crate) path: PathBuf,
pub(crate) definition: ServiceDefinition,
pub(crate) v2_path: Option<PathBuf>,
}
fn zccache_service_definition_v2(
daemon_binary: &Path,
) -> io::Result<protocol_v2::ServiceDefinition> {
let binary = std::fs::canonicalize(daemon_binary)?;
let binary_dir = binary.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"zccache-daemon binary path has no parent directory",
)
})?;
Ok(protocol_v2::ServiceDefinitionBuilder::shared_broker(
ZCCACHE_SERVICE_NAME,
binary.display().to_string(),
)
.per_version_binary_dir(binary_dir.display().to_string())
.min_version(crate::cli_core::core::VERSION)
.version_allow_list([crate::cli_core::core::VERSION])
.label("vendor", "zackees")
.label("package", "zccache")
.label("consumer", "zccache")
.label("running-process-tracker", "zackees/running-process#435")
.build())
}
pub(crate) fn zccache_service_definition(daemon_binary: &Path) -> io::Result<ServiceDefinition> {
let binary = std::fs::canonicalize(daemon_binary)?;
let binary_dir = binary.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"zccache-daemon binary path has no parent directory",
)
})?;
ServiceDefinitionBuilder::shared_broker(ZCCACHE_SERVICE_NAME, binary.display().to_string())
.per_version_binary_dir(binary_dir.display().to_string())
.min_version(crate::cli_core::core::VERSION)
.allow_version(crate::cli_core::core::VERSION)
.label("vendor", "zackees")
.label("package", "zccache")
.label("consumer", "zccache")
.label("running-process-tracker", "zackees/running-process#435")
.build()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
}
pub(crate) fn install_service_definition(
daemon_binary: &Path,
) -> io::Result<InstalledServiceDefinition> {
install_service_definition_to_dir(service_definition_dir(), daemon_binary)
}
pub(crate) fn install_service_definition_to_dir(
service_root: impl AsRef<Path>,
daemon_binary: &Path,
) -> io::Result<InstalledServiceDefinition> {
let service_root = service_root.as_ref();
let binary = std::fs::canonicalize(daemon_binary)?;
let binary_dir = binary.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"zccache-daemon binary path has no parent directory",
)
})?;
let definition = zccache_service_definition(daemon_binary)?;
let path =
ServiceDefinitionBuilder::shared_broker(ZCCACHE_SERVICE_NAME, binary.display().to_string())
.per_version_binary_dir(binary_dir.display().to_string())
.min_version(crate::cli_core::core::VERSION)
.allow_version(crate::cli_core::core::VERSION)
.label("vendor", "zackees")
.label("package", "zccache")
.label("consumer", "zccache")
.label("running-process-tracker", "zackees/running-process#435")
.install_in(service_root)
.map_err(|err| io::Error::other(err.to_string()))?;
let v2_path = match zccache_service_definition_v2(daemon_binary) {
Ok(def_v2) => match protocol_v2::write_service_definition_v2(service_root, &def_v2) {
Ok(path) => Some(path),
Err(err) => {
tracing::warn!(
"v2 ServiceDefinition install failed (non-fatal during rollout): {err}"
);
None
}
},
Err(err) => {
tracing::warn!("v2 ServiceDefinition build failed (non-fatal during rollout): {err}");
None
}
};
Ok(InstalledServiceDefinition {
path,
definition,
v2_path,
})
}
pub(crate) fn run_install_servicedef(
daemon_binary: Option<String>,
dir: Option<String>,
) -> ExitCode {
let daemon_binary = match daemon_binary {
Some(path) => PathBuf::from(path),
None => match super::daemon::find_daemon_binary() {
Some(path) => path.into_path_buf(),
None => {
eprintln!(
"zccache: cannot find zccache-daemon binary; pass --daemon-binary <path>"
);
return ExitCode::FAILURE;
}
},
};
let installed = match dir {
Some(dir) => install_service_definition_to_dir(dir, &daemon_binary),
None => install_service_definition(&daemon_binary),
};
match installed {
Ok(installed) => {
println!(
"installed {} (service `{}`, daemon `{}`)",
installed.path.display(),
installed.definition.service_name,
installed.definition.binary_path,
);
match &installed.v2_path {
Some(v2_path) => {
println!("dual-write v2: {}", v2_path.display());
}
None => {
println!("dual-write v2: skipped (see daemon log for cause)");
}
}
ExitCode::SUCCESS
}
Err(err) => {
eprintln!("zccache: failed to install service definition: {err}");
ExitCode::FAILURE
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use running_process::broker::protocol::BrokerIsolation;
use running_process::broker::server::ServiceDefinitionLoader;
use tempfile::TempDir;
fn fake_daemon_binary(root: &Path) -> PathBuf {
let binary = root.join(if cfg!(windows) {
"zccache-daemon.exe"
} else {
"zccache-daemon"
});
std::fs::write(&binary, b"stub").expect("fake daemon binary");
binary
}
#[test]
fn service_definition_declares_zccache_shared_broker() {
let temp = TempDir::new().expect("tempdir");
let daemon = fake_daemon_binary(temp.path());
let definition = zccache_service_definition(&daemon).expect("definition");
assert_eq!(definition.service_name, "zccache");
assert_eq!(definition.isolation, BrokerIsolation::SharedBroker as i32);
assert_eq!(definition.min_version, crate::cli_core::core::VERSION);
assert_eq!(definition.version_allow_list, [crate::cli_core::core::VERSION]);
assert_eq!(
definition
.labels
.get("running-process-tracker")
.map(String::as_str),
Some("zackees/running-process#435"),
);
assert_eq!(
definition.labels.get("consumer").map(String::as_str),
Some("zccache"),
);
}
#[test]
fn install_writes_loader_compatible_protobuf() {
let temp = TempDir::new().expect("tempdir");
let service_root = temp.path().join("services");
let daemon = fake_daemon_binary(temp.path());
let installed = install_service_definition_to_dir(&service_root, &daemon)
.expect("install service definition");
assert_eq!(installed.path, service_root.join("zccache.servicedef"));
let loaded = ServiceDefinitionLoader::new(&service_root)
.load("zccache")
.expect("load service definition");
assert_eq!(loaded, installed.definition);
}
#[test]
fn install_also_writes_v2_servicedef() {
use prost::Message;
let temp = TempDir::new().expect("tempdir");
let service_root = temp.path().join("services");
let daemon = fake_daemon_binary(temp.path());
let installed = install_service_definition_to_dir(&service_root, &daemon)
.expect("install service definition");
let v2_path = installed.v2_path.expect("v2 path must be present");
assert_eq!(v2_path, service_root.join("zccache.servicedef.v2"));
let bytes = std::fs::read(&v2_path).expect("read v2 file");
let decoded = protocol_v2::ServiceDefinition::decode(bytes.as_slice())
.expect("v2 ServiceDefinition decodes");
assert_eq!(decoded.service_name, "zccache");
assert_eq!(decoded.binary_path, installed.definition.binary_path);
assert_eq!(
decoded.isolation,
protocol_v2::BrokerIsolation::SharedBroker as i32,
"v2 must mirror v1's shared_broker isolation"
);
assert_eq!(decoded.min_version, crate::cli_core::core::VERSION);
assert_eq!(decoded.version_allow_list, vec![crate::cli_core::core::VERSION]);
assert_eq!(
decoded.labels.get("consumer").map(String::as_str),
Some("zccache")
);
assert_eq!(
decoded
.labels
.get("running-process-tracker")
.map(String::as_str),
Some("zackees/running-process#435")
);
}
#[test]
fn service_name_matches_backend_handle_probe() {
assert_eq!(ZCCACHE_SERVICE_NAME, "zccache");
}
}