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 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
98fn 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
135pub 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
152pub 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 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 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
221pub 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 while read_message(&mut reader).is_some() {}
256 return Ok(());
257 }
258}