Skip to main content

git_policy_cli/
service_client.rs

1//! What:
2//!  The wrapper's side of a commit through the resident service: connect, wait honestly while the operating system
3//!  starts the service, send the request, show the queue position, survive a service that dies mid-request, and turn
4//!  every failure into the plain text of `service_messages.rs`.
5//! Why:
6//!  A failure the system can resolve is resolved here without the user: a service that is starting is waited for, one
7//!  that crashed is reconnected to with the same request identity, one that is older than this wrapper is asked to retire
8//!  and is reached again once the manager has started the new one. Only what cannot be resolved ends the command.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! const result = await submit(config, request, stderr); // landed, or prepare again, or a ClientFailure
13//! ```
14
15use super::service_messages::{ClientFailure, Platform, STARTING_NOTICE, UPDATING_NOTICE, position_notice};
16use super::service_protocol::{
17    ClientMessage, CommitRequest, FailureCode, PROTOCOL_VERSION, ServiceMessage, decode_service, encode_client,
18};
19use super::service_server::Bind;
20use std::io::{BufRead, BufReader, Write};
21use std::os::unix::net::UnixStream;
22use std::time::{Duration, Instant};
23
24/// How long the client waits for the operating system to start the service.
25pub const START_WAIT: Duration = Duration::from_secs(10);
26
27/// How long a start takes before the client says it is starting the service.
28pub const START_NOTICE_GRACE: Duration = Duration::from_secs(1);
29
30/// How long a commit waits in the queue before the client gives up and says so.
31pub const COMMIT_WAIT: Duration = Duration::from_secs(10 * 60);
32
33/// How many times a request is sent again after the connection broke before the client says it cannot be sure.
34pub const RECONNECTS: usize = 5;
35
36/// How many times the client asks an older service to retire.
37pub const REPLACEMENTS: usize = 2;
38
39/// The pause between connection tries while the service starts.
40const CONNECT_PAUSE: Duration = Duration::from_millis(25);
41
42/// What the client may rely on the operating system to do.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum Activation {
45    /// The manager owns the socket and starts the service on demand.
46    Managed,
47    /// This platform cannot start the service on demand.
48    Unsupported(Platform),
49}
50
51/// What the client needs to reach the service.
52#[derive(Clone, Debug)]
53pub struct ClientConfig {
54    /// Where the service listens.
55    pub address: Bind,
56    /// What the operating system can do about a service that is not running.
57    pub activation: Activation,
58    /// Longest wait for a start.
59    pub start_wait: Duration,
60    /// Longest wait in the queue.
61    pub commit_wait: Duration,
62}
63
64/// What a request came to.
65#[derive(Clone, Debug, Eq, PartialEq)]
66pub enum Submitted {
67    /// The commit is on the branch.
68    Landed { commit: String, already: bool },
69    /// A named path changed on the branch: prepare again from this base and submit again with the same identity.
70    PrepareAgain { base: String },
71    /// The branch is no longer at the base of a commit that cannot be moved to another tip (an amend, or a commit Git built).
72    ///  The caller decides whether to build it again on the new tip or to stop.
73    Moved,
74}
75
76fn connect(address: &Bind) -> std::io::Result<UnixStream> {
77    return match address {
78        #[cfg(target_os = "linux")]
79        Bind::Abstract(name) => {
80            use std::os::linux::net::SocketAddrExt;
81            UnixStream::connect_addr(&std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?)
82        }
83        #[cfg(not(target_os = "linux"))]
84        Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
85        Bind::Path(path) => UnixStream::connect(path),
86        Bind::Inherited => Err(std::io::Error::other("a client does not inherit the socket")),
87    };
88}
89
90fn read_message(reader: &mut BufReader<UnixStream>) -> Option<ServiceMessage> {
91    let mut line: String = String::new();
92    return match reader.read_line(&mut line) {
93        Ok(read) if read > 0 => decode_service(line.trim_end_matches('\n')).ok(),
94        _ => None,
95    };
96}
97
98/// What:
99///  Connects, waiting for a start the operating system performs, and returns the stream with the service's version.
100/// Why:
101///   The wait is bounded and honest: one notice after the grace, then silence until the bound.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// const { stream, version } = await reach(config, notices)
106/// ```
107fn reach(config: &ClientConfig, notices: &mut dyn Write, announced: &mut bool) -> Result<(BufReader<UnixStream>, u32), ClientFailure> {
108    let started: Instant = Instant::now();
109    loop {
110        match connect(&config.address) {
111            Ok(stream) => {
112                let _timeout = stream.set_read_timeout(Some(config.start_wait));
113                let mut reader: BufReader<UnixStream> = BufReader::new(stream);
114                if let Some(ServiceMessage::Welcome { version }) = read_message(&mut reader) {
115                    let _widened = reader.get_ref().set_read_timeout(Some(config.commit_wait));
116                    return Ok((reader, version));
117                }
118            }
119            Err(_) => match config.activation {
120                Activation::Unsupported(platform) => return Err(ClientFailure::NoActivation(platform)),
121                Activation::Managed => {}
122            },
123        }
124        if started.elapsed() >= config.start_wait {
125            return Err(ClientFailure::DidNotStart);
126        }
127        if !*announced && started.elapsed() >= START_NOTICE_GRACE {
128            *announced = true;
129            let _written = writeln!(notices, "{STARTING_NOTICE}");
130        }
131        std::thread::sleep(CONNECT_PAUSE);
132    }
133}
134
135/// The user-facing failure for a code the service gave.
136pub fn failure_for(code: FailureCode, request: &CommitRequest) -> ClientFailure {
137    let repository: String = request.repository.display().to_string();
138    return match code {
139        FailureCode::RepositoryFull => ClientFailure::RepositoryFull(repository),
140        FailureCode::RepositoryReadOnly => ClientFailure::RepositoryReadOnly(repository),
141        FailureCode::IndexInUse => ClientFailure::IndexInUse(String::from("Another program")),
142        FailureCode::Rejected => ClientFailure::Refused,
143        FailureCode::BranchKeepsChanging => ClientFailure::BranchKeepsChanging {
144            branch: request.reference.trim_start_matches("refs/heads/").to_string(),
145            times: super::service_land::LANDING_ATTEMPTS,
146        },
147        FailureCode::BranchMoved => ClientFailure::BranchMoved { branch: request.reference.trim_start_matches("refs/heads/").to_string() },
148        FailureCode::Unavailable => ClientFailure::Unavailable,
149    };
150}
151
152/// What:
153///  Sends a commit request and waits until it is answered.
154/// Why:
155///   The same identity goes with every send, so a service that landed the commit and died before answering reports it
156///  as already landed, and nothing lands twice.
157///
158/// In TS you'd write (pseudocode):
159/// ```ts
160/// await submit(config, request, notices)
161/// ```
162pub fn submit(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write) -> Result<Submitted, ClientFailure> {
163    let line: String = encode_client(&ClientMessage::Commit(Box::new(request.clone())));
164    let waiting_since: Instant = Instant::now();
165    let mut announced: bool = false;
166    let mut replacements: usize = 0;
167    let mut breaks: usize = 0;
168    let mut last_position: Option<usize> = None;
169    loop {
170        if waiting_since.elapsed() >= config.commit_wait {
171            return Err(ClientFailure::WaitedTooLong { minutes: config.commit_wait.as_secs() / 60 });
172        }
173        let (mut reader, version) = reach(config, notices, &mut announced)?;
174        if version != PROTOCOL_VERSION {
175            // An older service is asked to finish and exit; the manager starts the current one for the next connection.
176            if version > PROTOCOL_VERSION || replacements >= REPLACEMENTS {
177                return Err(ClientFailure::NoCommonVersion);
178            }
179            replacements += 1;
180            let _written = writeln!(notices, "{UPDATING_NOTICE}");
181            let mut retire: String = encode_client(&ClientMessage::Retire);
182            retire.push('\n');
183            let _sent = reader.get_mut().write_all(retire.as_bytes());
184            std::thread::sleep(CONNECT_PAUSE * 4);
185            continue;
186        }
187        let mut payload: String = line.clone();
188        payload.push('\n');
189        if reader.get_mut().write_all(payload.as_bytes()).is_err() {
190            breaks += 1;
191            if breaks > RECONNECTS {
192                return Err(ClientFailure::MayNotHaveLanded);
193            }
194            continue;
195        }
196        loop {
197            match read_message(&mut reader) {
198                Some(ServiceMessage::Position { number, of }) => {
199                    if last_position != Some(number) {
200                        last_position = Some(number);
201                        let _written = writeln!(notices, "{}", position_notice(number, of));
202                    }
203                }
204                Some(ServiceMessage::Landed { commit, already, .. }) => return Ok(Submitted::Landed { commit, already }),
205                Some(ServiceMessage::RetryPrepare { base, .. }) => return Ok(Submitted::PrepareAgain { base }),
206                Some(ServiceMessage::Failed { code: FailureCode::BranchMoved, .. }) => return Ok(Submitted::Moved),
207                Some(ServiceMessage::Failed { code, .. }) => return Err(failure_for(code, request)),
208                Some(ServiceMessage::Welcome { .. }) => {}
209                None => break,
210            }
211        }
212        // The connection broke before an answer: the service died or was replaced. Send the same request again.
213        breaks += 1;
214        if breaks > RECONNECTS {
215            return Err(ClientFailure::MayNotHaveLanded);
216        }
217        std::thread::sleep(CONNECT_PAUSE * u32::try_from(breaks).unwrap_or(1));
218    }
219}
220
221/// What:
222///  Tells the service that work items were written for a repository, starting it through the operating system when it
223///  is not running, and returns once the service has taken the message.
224/// Why:
225///   The wrapper never starts the service or runs the work itself; the message is the only thing the service needs to
226///  look at the repository's work directory. The connection closes when the service has read the line, which is the
227///  confirmation that it will act.
228///
229/// In TS you'd write (pseudocode):
230/// ```ts
231/// await wake(config, commonDir)
232/// ```
233pub fn wake(config: &ClientConfig, common_dir: &std::path::Path, notices: &mut dyn Write) -> Result<(), ClientFailure> {
234    let mut line: String = encode_client(&ClientMessage::Work { common_dir: common_dir.to_path_buf() });
235    line.push('\n');
236    let mut announced: bool = false;
237    let mut replacements: usize = 0;
238    loop {
239        let (mut reader, version) = reach(config, notices, &mut announced)?;
240        if version != PROTOCOL_VERSION {
241            if version > PROTOCOL_VERSION || replacements >= REPLACEMENTS {
242                return Err(ClientFailure::NoCommonVersion);
243            }
244            replacements += 1;
245            let mut retire: String = encode_client(&ClientMessage::Retire);
246            retire.push('\n');
247            let _sent = reader.get_mut().write_all(retire.as_bytes());
248            std::thread::sleep(CONNECT_PAUSE * 4);
249            continue;
250        }
251        if reader.get_mut().write_all(line.as_bytes()).is_err() {
252            return Err(ClientFailure::Unavailable);
253        }
254        // The service closes the connection after it has read the line.
255        while read_message(&mut reader).is_some() {}
256        return Ok(());
257    }
258}