git_policy_cli/
service_provision.rs1use super::service_messages::{ClientFailure, Platform};
20use super::systemd_dbus::{Bus, CallError, EXCHANGE_WAIT};
21use std::path::{Path, PathBuf};
22
23pub const SOCKET_UNIT: &str = include_str!("service_units/cli-git.socket");
25
26pub const SERVICE_UNIT: &str = include_str!("service_units/cli-git.service");
28
29pub const SOCKET_UNIT_NAME: &str = "cli-git.socket";
31
32pub const SERVICE_UNIT_NAME: &str = "cli-git.service";
34
35pub const SERVICE_PROGRAM: &str = "cli-git-service";
37
38pub const OWN_MARKERS: [&str; 3] =
41 ["(cli-git-unit)", "Description=Commit service socket for the git wrapper", "Description=Commit service for the git wrapper"];
42
43const BINARY_PLACEHOLDER: &str = "@SERVICE_BINARY@";
44const GIT_PLACEHOLDER: &str = "@GIT@";
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct ProvisionHost {
49 pub runtime_dir: PathBuf,
51 pub user_unit_dir: Option<PathBuf>,
53 pub bus: PathBuf,
55 pub service_binary: PathBuf,
57 pub git: PathBuf,
59}
60
61pub trait Manager {
63 fn reload(&mut self) -> Result<(), CallError>;
65 fn start(&mut self, unit: &str) -> Result<(), CallError>;
67}
68
69impl Manager for Bus {
70 fn reload(&mut self) -> Result<(), CallError> {
71 return Bus::reload(self);
72 }
73
74 fn start(&mut self, unit: &str) -> Result<(), CallError> {
75 return Bus::start_unit(self, unit);
76 }
77}
78
79pub fn platform_without_manager() -> Platform {
81 if cfg!(windows) {
82 return Platform::Windows;
83 }
84 if cfg!(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly")) {
85 return Platform::Bsd;
86 }
87 if cfg!(target_os = "linux") {
88 return Platform::LinuxWithoutUserServices;
89 }
90 return Platform::Other;
91}
92
93pub fn unit_texts(host: &ProvisionHost) -> [(&'static str, String); 2] {
95 let service: String = SERVICE_UNIT
96 .replace(BINARY_PLACEHOLDER, host.service_binary.to_string_lossy().as_ref())
97 .replace(GIT_PLACEHOLDER, host.git.to_string_lossy().as_ref());
98 return [(SOCKET_UNIT_NAME, SOCKET_UNIT.to_string()), (SERVICE_UNIT_NAME, service)];
99}
100
101pub fn runtime_unit_dir(runtime_dir: &Path) -> PathBuf {
103 return runtime_dir.join("systemd").join("user");
104}
105
106static TEMPORARY_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
107
108fn write_atomically(path: &Path, text: &str) -> std::io::Result<()> {
110 let number: u64 = TEMPORARY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
111 let temporary: PathBuf = path.with_extension(format!("tmp-{}-{number}", std::process::id()));
112 std::fs::write(&temporary, text.as_bytes())?;
113 let renamed: std::io::Result<()> = std::fs::rename(&temporary, path);
114 if renamed.is_err() {
115 let _removed = std::fs::remove_file(&temporary);
116 }
117 return renamed;
118}
119
120fn clear_earlier_units(host: &ProvisionHost) -> Result<bool, ClientFailure> {
122 let Some(directory) = host.user_unit_dir.as_deref() else { return Ok(false) };
123 let mut removed: bool = false;
124 for name in [SOCKET_UNIT_NAME, SERVICE_UNIT_NAME] {
125 let path: PathBuf = directory.join(name);
126 let Ok(text) = std::fs::read_to_string(&path) else {
127 if path.exists() {
128 return Err(ClientFailure::UnitInTheWay(path.display().to_string()));
129 }
130 continue;
131 };
132 if !OWN_MARKERS.iter().any(|marker| { return text.contains(marker); }) {
133 return Err(ClientFailure::UnitInTheWay(path.display().to_string()));
134 }
135 std::fs::remove_file(&path).map_err(|error| { return ClientFailure::CouldNotStart(format!("{} cannot be removed: {error}", path.display())); })?;
136 removed = true;
137 }
138 let link: PathBuf = directory.join("sockets.target.wants").join(SOCKET_UNIT_NAME);
140 if std::fs::symlink_metadata(&link).is_ok_and(|meta| { return meta.file_type().is_symlink(); }) {
141 let _removed = std::fs::remove_file(&link);
142 removed = true;
143 }
144 return Ok(removed);
145}
146
147pub fn provision_with(host: &ProvisionHost, manager: &mut dyn Manager) -> Result<(), ClientFailure> {
159 let mut reload: bool = clear_earlier_units(host)?;
163 let directory: PathBuf = runtime_unit_dir(host.runtime_dir.as_path());
164 std::fs::create_dir_all(&directory).map_err(|error| { return ClientFailure::CouldNotStart(format!("{} cannot be created: {error}", directory.display())); })?;
165 for (name, text) in unit_texts(host) {
166 let path: PathBuf = directory.join(name);
167 let existed: bool = path.exists();
168 if std::fs::read_to_string(&path).is_ok_and(|current| { return current == text; }) {
169 continue;
170 }
171 write_atomically(&path, text.as_str())
172 .map_err(|error| { return ClientFailure::CouldNotStart(format!("{} cannot be written: {error}", path.display())); })?;
173 reload |= existed;
174 }
175 let refused = |error: CallError| -> ClientFailure {
176 return match error {
177 CallError::Manager { text, .. } if text.is_empty() => ClientFailure::CouldNotStart(String::from("the service manager refused the request")),
179 CallError::Manager { text, .. } => ClientFailure::CouldNotStart(format!("the service manager refused the request ({text})")),
180 CallError::Connection(_) | CallError::Authentication(_) | CallError::Malformed(_) => ClientFailure::NoActivation(platform_without_manager()),
181 };
182 };
183 if reload {
184 manager.reload().map_err(refused)?;
185 }
186 return manager.start(SOCKET_UNIT_NAME).map_err(refused);
187}
188
189pub fn provision(host: &ProvisionHost) -> Result<(), ClientFailure> {
191 let mut bus: Bus = Bus::connect(host.bus.as_path(), EXCHANGE_WAIT).map_err(|_| { return ClientFailure::NoActivation(platform_without_manager()); })?;
192 return provision_with(host, &mut bus);
193}
194
195#[cfg(test)]
197#[path = "service_provision_tests.rs"]
198mod tests;