Skip to main content

git_policy_cli/
service_route.rs

1//! What:
2//!  Which way a commit goes: through the resident service (already listening, or set up first by the wrapper), or the
3//!  way it went before the service existed.
4//! Why:
5//!  There is no switch and no install state for the person to manage (`doc/planning/cli-git-resident-service.md`,
6//!  "Provisioning"). The route follows the machine: a socket that is there means a service to use; a user session bus and
7//!  a service program next to the wrapper mean the wrapper can set the service up itself; anything else keeps the earlier
8//!  path until that path is retired, after which it is a plain failure. The decision reads the file system only and starts
9//!  no process.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! const route = chooseRoute({ runtimeDir, busAddress, configHome, homeDir, ownExecutable, realGit });
14//! ```
15
16use super::service_provision::{ProvisionHost, SERVICE_PROGRAM};
17use super::service_server::Bind;
18use super::systemd_dbus::session_bus_path;
19use std::path::{Path, PathBuf};
20
21/// The socket's place under the user's runtime directory.
22pub const SOCKET_RELATIVE: &str = "cli-git/service.sock";
23
24static OWN_EXECUTABLE: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
25
26/// Remembers where the running wrapper is, once, so the service program can be found beside it.
27///  Identification only: nothing is started from this path (`doc/planning/cli-git-no-self-invocation.md`); the path is
28///  read to name a sibling program in a unit file that the operating system's manager runs.
29pub fn remember_own_executable(path: &Path) {
30    let _first = OWN_EXECUTABLE.set(path.to_path_buf());
31}
32
33/// The wrapper's own path, when the process remembered it.
34pub fn remembered_own_executable() -> Option<&'static Path> {
35    return OWN_EXECUTABLE.get().map(PathBuf::as_path);
36}
37
38/// What the route decision reads.
39#[derive(Clone, Copy, Debug)]
40pub struct RouteInputs<'a> {
41    /// `XDG_RUNTIME_DIR`.
42    pub runtime_dir: Option<&'a Path>,
43    /// `DBUS_SESSION_BUS_ADDRESS`.
44    pub bus_address: Option<&'a str>,
45    /// `XDG_CONFIG_HOME`.
46    pub config_home: Option<&'a Path>,
47    /// `HOME`.
48    pub home: Option<&'a Path>,
49    /// The running wrapper's own executable.
50    pub own_executable: Option<&'a Path>,
51    /// The real Git the service will run.
52    pub real_git: &'a Path,
53}
54
55/// Which way a commit goes.
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub enum Route {
58    /// The earlier path.
59    Earlier,
60    /// Through the service at `address`, after provisioning when `provision` is present.
61    Service {
62        /// Where the service listens.
63        address: Bind,
64        /// What to set up first; absent when the socket already exists.
65        provision: Option<ProvisionHost>,
66    },
67}
68
69/// The directory a user's own unit files live in.
70pub fn user_unit_dir(config_home: Option<&Path>, home: Option<&Path>) -> Option<PathBuf> {
71    return config_home
72        .map(|directory| { return directory.join("systemd").join("user"); })
73        .or_else(|| { return home.map(|directory| { return directory.join(".config").join("systemd").join("user"); }); });
74}
75
76/// What:
77///  Decides the route.
78/// Why:
79///   A socket that exists is used as it is, whoever made it; otherwise provisioning needs the session bus socket and the
80///  service program, and without both the earlier path stays.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// chooseRoute(inputs)
85/// ```
86pub fn choose_route(inputs: &RouteInputs<'_>) -> Route {
87    let Some(runtime_dir) = inputs.runtime_dir else { return Route::Earlier };
88    let socket: PathBuf = runtime_dir.join(SOCKET_RELATIVE);
89    if socket.exists() {
90        return Route::Service { address: Bind::Path(socket), provision: None };
91    }
92    let Some(bus) = session_bus_path(inputs.bus_address, Some(runtime_dir)).filter(|path| { return path.exists(); }) else { return Route::Earlier };
93    let Some(service_binary) = inputs
94        .own_executable
95        .and_then(Path::parent)
96        .map(|directory| { return directory.join(SERVICE_PROGRAM); })
97        .filter(|path| { return path.is_file(); })
98    else {
99        return Route::Earlier;
100    };
101    let host: ProvisionHost = ProvisionHost {
102        runtime_dir: runtime_dir.to_path_buf(),
103        user_unit_dir: user_unit_dir(inputs.config_home, inputs.home),
104        bus,
105        service_binary,
106        git: inputs.real_git.to_path_buf(),
107    };
108    return Route::Service { address: Bind::Path(socket), provision: Some(host) };
109}
110
111/// What `git cli-git service status` prints and exits with.
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct StatusReport {
114    /// Exit status: 0 when the service answered.
115    pub code: i32,
116    /// Standard output.
117    pub stdout: String,
118    /// Standard error.
119    pub stderr: String,
120}
121
122/// What:
123///  Says whether the commit service answers on its socket.
124/// Why:
125///   A diagnostic only: there is nothing to install or start by hand. Connecting is what a commit does, so a service
126///  that the manager would start on demand is started by this check too.
127///
128/// In TS you'd write (pseudocode):
129/// ```ts
130/// serviceStatus(runtimeDir)
131/// ```
132pub fn service_status(runtime_dir: Option<&Path>) -> StatusReport {
133    let answered: bool = runtime_dir
134        .map(|directory| { return directory.join(SOCKET_RELATIVE); })
135        .is_some_and(|socket| { return std::os::unix::net::UnixStream::connect(socket).is_ok(); });
136    return if answered {
137        StatusReport { code: 0, stdout: String::from("The commit service is running and answers.\n"), stderr: String::new() }
138    } else {
139        StatusReport {
140            code: 1,
141            stdout: String::new(),
142            stderr: String::from(
143                "The commit service does not answer here. It sets itself up the first time a commit needs it, so there is nothing to \
144                 install; if a commit fails, its message says what to do.\n",
145            ),
146        }
147    };
148}
149
150/// Route controls stay out of the release executable.
151#[cfg(test)]
152#[path = "service_route_tests.rs"]
153mod tests;