1use super::service_messages::{ClientFailure, Platform};
30use super::service_client::ask_to_retire;
31use super::service_instance::{LEGACY_SOCKET_RELATIVE, service_unit_instance, socket_path, socket_unit_instance};
32use super::systemd_dbus::{Bus, CallError, EXCHANGE_WAIT, NO_SUCH_UNIT};
33use std::path::{Path, PathBuf};
34
35pub const SOCKET_UNIT: &str = include_str!("service_units/cli-git@.socket");
37
38pub const SERVICE_UNIT: &str = include_str!("service_units/cli-git@.service");
40
41pub const SOCKET_UNIT_NAME: &str = "cli-git@.socket";
43
44pub const SERVICE_UNIT_NAME: &str = "cli-git@.service";
46
47pub const LEGACY_SOCKET_UNIT_NAME: &str = "cli-git.socket";
49
50pub const LEGACY_SERVICE_UNIT_NAME: &str = "cli-git.service";
52
53pub const SERVICE_PROGRAM: &str = "cli-git-service";
55
56pub const OWN_MARKERS: [&str; 3] =
59 ["(cli-git-unit)", "Description=Commit service socket for the git wrapper", "Description=Commit service for the git wrapper"];
60
61const BINARY_PLACEHOLDER: &str = "@SERVICE_BINARY@";
62const GIT_PLACEHOLDER: &str = "@GIT@";
63
64#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct ProvisionHost {
67 pub runtime_dir: PathBuf,
69 pub user_unit_dir: Option<PathBuf>,
71 pub bus: PathBuf,
73 pub service_binary: PathBuf,
75 pub git: PathBuf,
77 pub instance: String,
79 pub retirement_wait: std::time::Duration,
81}
82
83pub const RETIREMENT_WAIT: std::time::Duration = std::time::Duration::from_secs(10);
86
87pub trait Manager {
89 fn reload(&mut self) -> Result<(), CallError>;
91 fn reset_failed(&mut self, unit: &str) -> Result<(), CallError>;
93 fn start(&mut self, unit: &str) -> Result<(), CallError>;
95 fn stop(&mut self, unit: &str) -> Result<(), CallError>;
97 fn active_state(&mut self, unit: &str) -> Result<Option<String>, CallError>;
100}
101
102impl Manager for Bus {
103 fn reload(&mut self) -> Result<(), CallError> {
104 return Bus::reload(self);
105 }
106
107 fn reset_failed(&mut self, unit: &str) -> Result<(), CallError> {
108 return Bus::reset_failed(self, unit);
109 }
110
111 fn start(&mut self, unit: &str) -> Result<(), CallError> {
112 return Bus::start_unit(self, unit);
113 }
114
115 fn stop(&mut self, unit: &str) -> Result<(), CallError> {
116 return Bus::stop_unit(self, unit);
117 }
118
119 fn active_state(&mut self, unit: &str) -> Result<Option<String>, CallError> {
120 return Bus::active_state(self, unit);
121 }
122}
123
124pub fn platform_without_manager() -> Platform {
126 if cfg!(windows) {
127 return Platform::Windows;
128 }
129 if cfg!(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly")) {
130 return Platform::Bsd;
131 }
132 if cfg!(target_os = "linux") {
133 return Platform::LinuxWithoutUserServices;
134 }
135 return Platform::Other;
136}
137
138pub fn unit_texts(host: &ProvisionHost) -> [(&'static str, String); 2] {
140 let service: String = SERVICE_UNIT
141 .replace(BINARY_PLACEHOLDER, host.service_binary.to_string_lossy().as_ref())
142 .replace(GIT_PLACEHOLDER, host.git.to_string_lossy().as_ref());
143 return [(SOCKET_UNIT_NAME, SOCKET_UNIT.to_string()), (SERVICE_UNIT_NAME, service)];
144}
145
146pub fn runtime_unit_dir(runtime_dir: &Path) -> PathBuf {
148 return runtime_dir.join("systemd").join("user");
149}
150
151pub fn in_place(host: &ProvisionHost) -> bool {
164 let directory: PathBuf = runtime_unit_dir(host.runtime_dir.as_path());
165 let current = |(name, text): (&str, String)| -> bool {
166 return std::fs::read_to_string(directory.join(name)).is_ok_and(|installed| { return installed == text; });
167 };
168 let earlier_left: bool = [LEGACY_SOCKET_UNIT_NAME, LEGACY_SERVICE_UNIT_NAME].iter().any(|name| { return directory.join(name).exists(); })
169 || host.runtime_dir.join(LEGACY_SOCKET_RELATIVE).exists();
170 return unit_texts(host).into_iter().all(current)
171 && socket_path(host.runtime_dir.as_path(), host.instance.as_str()).exists()
172 && !earlier_left;
173}
174
175static TEMPORARY_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
176
177fn write_atomically(path: &Path, text: &str) -> std::io::Result<()> {
179 let number: u64 = TEMPORARY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
180 let temporary: PathBuf = path.with_extension(format!("tmp-{}-{number}", std::process::id()));
181 std::fs::write(&temporary, text.as_bytes())?;
182 let renamed: std::io::Result<()> = std::fs::rename(&temporary, path);
183 if renamed.is_err() {
184 let _removed = std::fs::remove_file(&temporary);
185 }
186 return renamed;
187}
188
189fn clear_earlier_units(host: &ProvisionHost) -> Result<bool, ClientFailure> {
191 let Some(directory) = host.user_unit_dir.as_deref() else { return Ok(false) };
192 let mut removed: bool = false;
193 for name in [LEGACY_SOCKET_UNIT_NAME, LEGACY_SERVICE_UNIT_NAME] {
194 let path: PathBuf = directory.join(name);
195 let Ok(text) = std::fs::read_to_string(&path) else {
196 if path.exists() {
197 return Err(ClientFailure::UnitInTheWay(path.display().to_string()));
198 }
199 continue;
200 };
201 if !OWN_MARKERS.iter().any(|marker| { return text.contains(marker); }) {
202 return Err(ClientFailure::UnitInTheWay(path.display().to_string()));
203 }
204 std::fs::remove_file(&path).map_err(|error| { return ClientFailure::CouldNotStart(format!("{} cannot be removed: {error}", path.display())); })?;
205 removed = true;
206 }
207 let link: PathBuf = directory.join("sockets.target.wants").join(LEGACY_SOCKET_UNIT_NAME);
209 if std::fs::symlink_metadata(&link).is_ok_and(|meta| { return meta.file_type().is_symlink(); }) {
210 let _removed = std::fs::remove_file(&link);
211 removed = true;
212 }
213 return Ok(removed);
214}
215
216fn retire_earlier_service(host: &ProvisionHost, manager: &mut dyn Manager) -> Result<bool, ClientFailure> {
230 let directory: PathBuf = runtime_unit_dir(host.runtime_dir.as_path());
231 let directories: Vec<PathBuf> = std::iter::once(directory.clone()).chain(host.user_unit_dir.clone()).collect();
233 let socket_file: PathBuf = host.runtime_dir.join(LEGACY_SOCKET_RELATIVE);
234 let ours = |name: &str| -> bool {
235 return directories.iter().any(|found| {
236 return std::fs::read_to_string(found.join(name)).is_ok_and(|text| { return OWN_MARKERS.iter().any(|marker| { return text.contains(marker); }); });
237 });
238 };
239 let named_anywhere = |name: &str| -> bool { return directories.iter().any(|found| { return found.join(name).exists(); }); };
240 let both: [&str; 2] = [LEGACY_SOCKET_UNIT_NAME, LEGACY_SERVICE_UNIT_NAME];
241 let mut units: Vec<&str> = both.into_iter().filter(|name| { return ours(name); }).collect();
242 let had_files: bool = !units.is_empty();
243 if units.is_empty() && socket_file.exists() && !both.iter().any(|name| { return named_anywhere(name); }) {
246 units = both.to_vec();
247 }
248 if units.is_empty() {
249 return Ok(false);
250 }
251 ask_to_retire(socket_file.as_path());
252 for name in &units {
254 match manager.stop(name) {
255 Ok(()) => {}
256 Err(CallError::Manager { name: error, .. }) if error == NO_SUCH_UNIT => {}
258 Err(error) => return Err(refusal(error)),
259 }
260 }
261 wait_until_stopped(host, manager, units.as_slice())?;
262 for name in &units {
263 let _removed = std::fs::remove_file(directory.join(name));
264 }
265 let _socket_removed = std::fs::remove_file(&socket_file);
266 return Ok(had_files);
267}
268
269fn wait_until_stopped(host: &ProvisionHost, manager: &mut dyn Manager, units: &[&str]) -> Result<(), ClientFailure> {
282 let deadline: std::time::Instant = std::time::Instant::now() + host.retirement_wait;
283 loop {
284 let mut running: Option<&str> = None;
285 for name in units {
286 let state: Option<String> = manager.active_state(name).map_err(refusal)?;
287 if state.is_some_and(|text| { return !matches!(text.as_str(), "inactive" | "failed"); }) {
288 running = Some(name);
289 }
290 }
291 let Some(unit) = running else { return Ok(()) };
292 if std::time::Instant::now() >= deadline {
293 return Err(ClientFailure::CouldNotStart(format!(
294 "the earlier commit service ({unit}) is still stopping after {:.1} seconds; run the commit again in a moment, or stop it with `systemctl --user stop {LEGACY_SERVICE_UNIT_NAME} {LEGACY_SOCKET_UNIT_NAME}`",
295 host.retirement_wait.as_secs_f64()
296 )));
297 }
298 std::thread::sleep(RETIREMENT_POLL);
299 }
300}
301
302const RETIREMENT_POLL: std::time::Duration = std::time::Duration::from_millis(50);
304
305fn refusal(error: CallError) -> ClientFailure {
307 return match error {
308 CallError::Manager { text, .. } if text.is_empty() => ClientFailure::CouldNotStart(String::from("the service manager refused the request")),
309 CallError::Manager { text, .. } => ClientFailure::CouldNotStart(format!("the service manager refused the request ({text})")),
310 CallError::Connection(_) | CallError::Authentication(_) | CallError::Malformed(_) => ClientFailure::NoActivation(platform_without_manager()),
311 };
312}
313
314pub fn provision_with(host: &ProvisionHost, manager: &mut dyn Manager) -> Result<(), ClientFailure> {
326 let mut reload: bool = retire_earlier_service(host, manager)?;
330 reload |= clear_earlier_units(host)?;
331 let directory: PathBuf = runtime_unit_dir(host.runtime_dir.as_path());
332 std::fs::create_dir_all(&directory).map_err(|error| { return ClientFailure::CouldNotStart(format!("{} cannot be created: {error}", directory.display())); })?;
333 let mut known: bool = false;
335 let mut written: Vec<PathBuf> = Vec::new();
336 for (name, text) in unit_texts(host) {
337 let path: PathBuf = directory.join(name);
338 let existed: bool = path.exists();
339 known |= existed;
340 if std::fs::read_to_string(&path).is_ok_and(|current| { return current == text; }) {
341 continue;
342 }
343 write_atomically(&path, text.as_str())
344 .map_err(|error| { return ClientFailure::CouldNotStart(format!("{} cannot be written: {error}", path.display())); })?;
345 written.push(path);
346 reload |= existed;
347 }
348 let asked: Result<(), CallError> = ask_manager(manager, reload, known, host.instance.as_str());
349 if let Err(error) = asked {
350 for path in &written {
353 let _removed = std::fs::remove_file(path);
354 }
355 return Err(refusal(error));
356 }
357 return Ok(());
358}
359
360fn ask_manager(manager: &mut dyn Manager, reload: bool, known: bool, instance: &str) -> Result<(), CallError> {
363 if reload {
364 manager.reload()?;
365 }
366 if known {
367 for unit in [service_unit_instance(instance), socket_unit_instance(instance)] {
370 let _reset = manager.reset_failed(unit.as_str());
371 }
372 }
373 return manager.start(socket_unit_instance(instance).as_str());
374}
375
376pub fn provision(host: &ProvisionHost) -> Result<(), ClientFailure> {
378 if in_place(host) {
379 return Ok(());
380 }
381 let mut bus: Bus = Bus::connect(host.bus.as_path(), EXCHANGE_WAIT).map_err(|_| { return ClientFailure::NoActivation(platform_without_manager()); })?;
382 return provision_with(host, &mut bus);
383}
384
385#[cfg(test)]
387#[path = "service_provision_tests.rs"]
388mod tests;