git_policy_cli/
service_client.rs1use 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
24pub const START_WAIT: Duration = Duration::from_secs(10);
26
27pub const START_NOTICE_GRACE: Duration = Duration::from_secs(1);
29
30pub const COMMIT_WAIT: Duration = Duration::from_secs(10 * 60);
32
33pub const RECONNECTS: usize = 5;
35
36pub const REPLACEMENTS: usize = 2;
38
39const CONNECT_PAUSE: Duration = Duration::from_millis(25);
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum Activation {
45 Managed,
47 Unsupported(Platform),
49}
50
51#[derive(Clone, Debug)]
53pub struct ClientConfig {
54 pub address: Bind,
56 pub activation: Activation,
58 pub start_wait: Duration,
60 pub commit_wait: Duration,
62}
63
64#[derive(Clone, Debug, Eq, PartialEq)]
66pub enum Submitted {
67 Landed { commit: String, already: bool },
69 PrepareAgain { base: String },
71}
72
73fn connect(address: &Bind) -> std::io::Result<UnixStream> {
74 return match address {
75 #[cfg(target_os = "linux")]
76 Bind::Abstract(name) => {
77 use std::os::linux::net::SocketAddrExt;
78 UnixStream::connect_addr(&std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?)
79 }
80 #[cfg(not(target_os = "linux"))]
81 Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
82 Bind::Path(path) => UnixStream::connect(path),
83 Bind::Inherited => Err(std::io::Error::other("a client does not inherit the socket")),
84 };
85}
86
87fn read_message(reader: &mut BufReader<UnixStream>) -> Option<ServiceMessage> {
88 let mut line: String = String::new();
89 return match reader.read_line(&mut line) {
90 Ok(read) if read > 0 => decode_service(line.trim_end_matches('\n')).ok(),
91 _ => None,
92 };
93}
94
95fn reach(config: &ClientConfig, notices: &mut dyn Write, announced: &mut bool) -> Result<(BufReader<UnixStream>, u32), ClientFailure> {
105 let started: Instant = Instant::now();
106 loop {
107 match connect(&config.address) {
108 Ok(stream) => {
109 let _timeout = stream.set_read_timeout(Some(config.start_wait));
110 let mut reader: BufReader<UnixStream> = BufReader::new(stream);
111 if let Some(ServiceMessage::Welcome { version }) = read_message(&mut reader) {
112 let _widened = reader.get_ref().set_read_timeout(Some(config.commit_wait));
113 return Ok((reader, version));
114 }
115 }
116 Err(_) => match config.activation {
117 Activation::Unsupported(platform) => return Err(ClientFailure::NoActivation(platform)),
118 Activation::Managed => {}
119 },
120 }
121 if started.elapsed() >= config.start_wait {
122 return Err(ClientFailure::DidNotStart);
123 }
124 if !*announced && started.elapsed() >= START_NOTICE_GRACE {
125 *announced = true;
126 let _written = writeln!(notices, "{STARTING_NOTICE}");
127 }
128 std::thread::sleep(CONNECT_PAUSE);
129 }
130}
131
132pub fn failure_for(code: FailureCode, request: &CommitRequest) -> ClientFailure {
134 let repository: String = request.repository.display().to_string();
135 return match code {
136 FailureCode::RepositoryFull => ClientFailure::RepositoryFull(repository),
137 FailureCode::RepositoryReadOnly => ClientFailure::RepositoryReadOnly(repository),
138 FailureCode::IndexInUse => ClientFailure::IndexInUse(String::from("Another program")),
139 FailureCode::Rejected => ClientFailure::Refused,
140 FailureCode::BranchKeepsChanging => ClientFailure::BranchKeepsChanging {
141 branch: request.reference.trim_start_matches("refs/heads/").to_string(),
142 times: super::service_land::LANDING_ATTEMPTS,
143 },
144 FailureCode::Unavailable => ClientFailure::Unavailable,
145 };
146}
147
148pub fn submit(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write) -> Result<Submitted, ClientFailure> {
159 let line: String = encode_client(&ClientMessage::Commit(Box::new(request.clone())));
160 let waiting_since: Instant = Instant::now();
161 let mut announced: bool = false;
162 let mut replacements: usize = 0;
163 let mut breaks: usize = 0;
164 let mut last_position: Option<usize> = None;
165 loop {
166 if waiting_since.elapsed() >= config.commit_wait {
167 return Err(ClientFailure::WaitedTooLong { minutes: config.commit_wait.as_secs() / 60 });
168 }
169 let (mut reader, version) = reach(config, notices, &mut announced)?;
170 if version != PROTOCOL_VERSION {
171 if version > PROTOCOL_VERSION || replacements >= REPLACEMENTS {
173 return Err(ClientFailure::NoCommonVersion);
174 }
175 replacements += 1;
176 let _written = writeln!(notices, "{UPDATING_NOTICE}");
177 let mut retire: String = encode_client(&ClientMessage::Retire);
178 retire.push('\n');
179 let _sent = reader.get_mut().write_all(retire.as_bytes());
180 std::thread::sleep(CONNECT_PAUSE * 4);
181 continue;
182 }
183 let mut payload: String = line.clone();
184 payload.push('\n');
185 if reader.get_mut().write_all(payload.as_bytes()).is_err() {
186 breaks += 1;
187 if breaks > RECONNECTS {
188 return Err(ClientFailure::MayNotHaveLanded);
189 }
190 continue;
191 }
192 loop {
193 match read_message(&mut reader) {
194 Some(ServiceMessage::Position { number, of }) => {
195 if last_position != Some(number) {
196 last_position = Some(number);
197 let _written = writeln!(notices, "{}", position_notice(number, of));
198 }
199 }
200 Some(ServiceMessage::Landed { commit, already, .. }) => return Ok(Submitted::Landed { commit, already }),
201 Some(ServiceMessage::RetryPrepare { base, .. }) => return Ok(Submitted::PrepareAgain { base }),
202 Some(ServiceMessage::Failed { code, .. }) => return Err(failure_for(code, request)),
203 Some(ServiceMessage::Welcome { .. }) => {}
204 None => break,
205 }
206 }
207 breaks += 1;
209 if breaks > RECONNECTS {
210 return Err(ClientFailure::MayNotHaveLanded);
211 }
212 std::thread::sleep(CONNECT_PAUSE * u32::try_from(breaks).unwrap_or(1));
213 }
214}
215
216pub fn wake(config: &ClientConfig, common_dir: &std::path::Path, notices: &mut dyn Write) -> Result<(), ClientFailure> {
229 let mut line: String = encode_client(&ClientMessage::Work { common_dir: common_dir.to_path_buf() });
230 line.push('\n');
231 let mut announced: bool = false;
232 let mut replacements: usize = 0;
233 loop {
234 let (mut reader, version) = reach(config, notices, &mut announced)?;
235 if version != PROTOCOL_VERSION {
236 if version > PROTOCOL_VERSION || replacements >= REPLACEMENTS {
237 return Err(ClientFailure::NoCommonVersion);
238 }
239 replacements += 1;
240 let mut retire: String = encode_client(&ClientMessage::Retire);
241 retire.push('\n');
242 let _sent = reader.get_mut().write_all(retire.as_bytes());
243 std::thread::sleep(CONNECT_PAUSE * 4);
244 continue;
245 }
246 if reader.get_mut().write_all(line.as_bytes()).is_err() {
247 return Err(ClientFailure::Unavailable);
248 }
249 while read_message(&mut reader).is_some() {}
251 return Ok(());
252 }
253}