1use super::commit_timing::{ReachTrace, SubmitTrace};
17use super::service_messages::{ClientFailure, Platform, STARTING_NOTICE, UPDATING_NOTICE, position_notice};
18use super::service_protocol::{
19 ClientMessage, CommitRequest, FailureCode, LandedCapture, PROTOCOL_VERSION, RELEASE, ServiceMessage, decode_service, encode_client, encode_retire,
20};
21use super::service_review_key::ReviewKey;
22use super::service_server::Bind;
23use std::io::{BufRead, BufReader, Write};
24use std::os::unix::net::UnixStream;
25use std::time::{Duration, Instant};
26
27pub const START_WAIT: Duration = Duration::from_secs(10);
29
30pub const START_NOTICE_GRACE: Duration = Duration::from_secs(1);
32
33pub const COMMIT_WAIT: Duration = Duration::from_secs(10 * 60);
35
36pub const RECONNECTS: usize = 5;
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, landed: Vec<LandedCapture> },
72 Moved,
75}
76
77fn connect(address: &Bind) -> std::io::Result<UnixStream> {
78 return match address {
79 #[cfg(target_os = "linux")]
80 Bind::Abstract(name) => {
81 use std::os::linux::net::SocketAddrExt;
82 UnixStream::connect_addr(&std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?)
83 }
84 #[cfg(not(target_os = "linux"))]
85 Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
86 Bind::Path(path) => UnixStream::connect(path),
87 Bind::Inherited => Err(std::io::Error::other("a client does not inherit the socket")),
88 };
89}
90
91fn read_message(reader: &mut BufReader<UnixStream>) -> Option<ServiceMessage> {
92 let mut line: String = String::new();
93 return match reader.read_line(&mut line) {
94 Ok(read) if read > 0 => decode_service(line.trim_end_matches('\n')).ok(),
95 _ => None,
96 };
97}
98
99fn reach(
109 config: &ClientConfig,
110 notices: &mut dyn Write,
111 announced: &mut bool,
112 trace: &mut ReachTrace,
113) -> Result<(BufReader<UnixStream>, u32, Option<String>), ClientFailure> {
114 let started: Instant = Instant::now();
115 let mut refused_here: bool = false;
116 loop {
117 trace.tries += 1;
118 match connect(&config.address) {
119 Ok(stream) => {
120 let _timeout = stream.set_read_timeout(Some(config.start_wait));
121 let mut reader: BufReader<UnixStream> = BufReader::new(stream);
122 if let Some(ServiceMessage::Welcome { version, release }) = read_message(&mut reader) {
123 let _widened = reader.get_ref().set_read_timeout(Some(config.commit_wait));
124 trace.saw_release(release.as_deref());
125 if refused_here {
126 trace.start_waited += started.elapsed();
127 }
128 return Ok((reader, version, release));
129 }
130 trace.refused += 1;
132 refused_here = true;
133 }
134 Err(_) => match config.activation {
135 Activation::Unsupported(platform) => return Err(ClientFailure::NoActivation(platform)),
136 Activation::Managed => {
137 trace.refused += 1;
138 refused_here = true;
139 }
140 },
141 }
142 if started.elapsed() >= config.start_wait {
143 return Err(ClientFailure::DidNotStart);
144 }
145 if !*announced && started.elapsed() >= START_NOTICE_GRACE {
146 *announced = true;
147 let _written = writeln!(notices, "{STARTING_NOTICE}");
148 }
149 std::thread::sleep(CONNECT_PAUSE);
150 }
151}
152
153fn reach_current(config: &ClientConfig, notices: &mut dyn Write, announced: &mut bool, trace: &mut ReachTrace) -> Result<BufReader<UnixStream>, ClientFailure> {
169 let began: Instant = Instant::now();
170 let reached = reach_current_untimed(config, notices, announced, trace);
171 trace.elapsed += began.elapsed();
172 return reached;
173}
174
175fn reach_current_untimed(config: &ClientConfig, notices: &mut dyn Write, announced: &mut bool, trace: &mut ReachTrace) -> Result<BufReader<UnixStream>, ClientFailure> {
177 let mut give_up_at: Option<Instant> = None;
178 loop {
179 let (mut reader, version, release) = reach(config, notices, announced, trace)?;
180 if version == PROTOCOL_VERSION && release.as_deref() == Some(RELEASE) {
181 trace.answered_by = release;
182 return Ok(reader);
183 }
184 if version > PROTOCOL_VERSION {
185 return Err(ClientFailure::NoCommonVersion);
186 }
187 let deadline: Instant = *give_up_at.get_or_insert_with(|| {
188 let _written = writeln!(notices, "{UPDATING_NOTICE}");
189 return Instant::now() + config.start_wait;
190 });
191 if Instant::now() >= deadline {
192 return Err(ClientFailure::OtherRelease);
193 }
194 let mut retire: String = encode_retire(version);
196 retire.push('\n');
197 let _sent = reader.get_mut().write_all(retire.as_bytes());
198 trace.retires_sent += 1;
199 std::thread::sleep(CONNECT_PAUSE * 4);
200 }
201}
202
203pub fn failure_for(code: FailureCode, request: &CommitRequest) -> ClientFailure {
205 let repository: String = request.repository.display().to_string();
206 return match code {
207 FailureCode::RepositoryFull => ClientFailure::RepositoryFull(repository),
208 FailureCode::RepositoryReadOnly => ClientFailure::RepositoryReadOnly(repository),
209 FailureCode::IndexInUse => ClientFailure::IndexInUse(String::from("Another program")),
210 FailureCode::Rejected => ClientFailure::Refused,
211 FailureCode::BranchKeepsChanging => ClientFailure::BranchKeepsChanging {
212 branch: request.reference.trim_start_matches("refs/heads/").to_string(),
213 times: super::service_land::LANDING_ATTEMPTS,
214 },
215 FailureCode::BranchMoved => ClientFailure::BranchMoved { branch: request.reference.trim_start_matches("refs/heads/").to_string() },
216 FailureCode::Unavailable => ClientFailure::Unavailable,
217 };
218}
219
220pub fn submit(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write) -> Result<Submitted, ClientFailure> {
231 return submit_traced(config, request, notices, &mut SubmitTrace::default());
232}
233
234pub fn submit_traced(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write, trace: &mut SubmitTrace) -> Result<Submitted, ClientFailure> {
251 let began: Instant = Instant::now();
252 let answered = submit_loop(config, request, notices, trace);
253 trace.total += began.elapsed();
254 return answered;
255}
256
257fn submit_loop(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write, trace: &mut SubmitTrace) -> Result<Submitted, ClientFailure> {
259 let line: String = encode_client(&ClientMessage::Commit(Box::new(request.clone())));
260 let waiting_since: Instant = Instant::now();
261 let mut announced: bool = false;
262 let mut breaks: usize = 0;
263 let mut last_position: Option<usize> = None;
264 let mut holder: Option<String> = None;
265 loop {
266 if waiting_since.elapsed() >= config.commit_wait {
267 return Err(ClientFailure::WaitedTooLong { minutes: config.commit_wait.as_secs() / 60 });
268 }
269 let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced, &mut trace.reach)?;
270 let mut payload: String = line.clone();
271 payload.push('\n');
272 if reader.get_mut().write_all(payload.as_bytes()).is_err() {
273 breaks += 1;
274 trace.reconnects += 1;
275 if breaks > RECONNECTS {
276 return Err(ClientFailure::MayNotHaveLanded);
277 }
278 continue;
279 }
280 trace.sends += 1;
281 let written_at: Instant = Instant::now();
282 let answered: Option<Result<Submitted, ClientFailure>> = loop {
283 match read_message(&mut reader) {
284 Some(ServiceMessage::Position { number, of }) => {
285 if last_position != Some(number) {
286 last_position = Some(number);
287 trace.positions.push(number);
288 let _written = writeln!(notices, "{}", position_notice(number, of));
289 }
290 }
291 Some(ServiceMessage::Landed { commit, already, .. }) => break Some(Ok(Submitted::Landed { commit, already })),
292 Some(ServiceMessage::RetryPrepare { base, landed, .. }) => break Some(Ok(Submitted::PrepareAgain { base, landed })),
293 Some(ServiceMessage::Failed { code: FailureCode::BranchMoved, .. }) => break Some(Ok(Submitted::Moved)),
294 Some(ServiceMessage::Failed { code: FailureCode::IndexInUse, .. }) => {
295 break Some(Err(ClientFailure::IndexInUse(holder.take().unwrap_or_else(|| { return String::from("Another program"); }))));
296 }
297 Some(ServiceMessage::Failed { code, .. }) => break Some(Err(failure_for(code, request))),
298 Some(ServiceMessage::Waiting { text }) => {
299 trace.waiting_notices += 1;
300 let _written = write!(notices, "{text}");
301 }
302 Some(ServiceMessage::Holder { who }) => holder = Some(who),
303 Some(ServiceMessage::Welcome { .. }) => {}
304 None => break None,
305 }
306 };
307 trace.answer_wait += written_at.elapsed();
308 if let Some(done) = answered {
309 return done;
310 }
311 breaks += 1;
313 if breaks > RECONNECTS {
314 return Err(ClientFailure::MayNotHaveLanded);
315 }
316 std::thread::sleep(CONNECT_PAUSE * u32::try_from(breaks).unwrap_or(1));
317 }
318}
319
320pub fn ask_to_retire(socket: &std::path::Path) {
332 let Ok(stream) = UnixStream::connect(socket) else { return };
333 let _timeout = stream.set_read_timeout(Some(START_WAIT));
334 let mut reader: BufReader<UnixStream> = BufReader::new(stream);
335 let Some(ServiceMessage::Welcome { version, .. }) = read_message(&mut reader) else { return };
336 let mut retire: String = encode_retire(version);
337 retire.push('\n');
338 let _sent = reader.get_mut().write_all(retire.as_bytes());
339}
340
341pub struct ScratchWatch {
344 _connection: UnixStream,
346}
347
348pub fn watch_scratch(config: &ClientConfig, git_dir: &std::path::Path, path: &std::path::Path) -> Option<ScratchWatch> {
363 let mut connection: UnixStream = connect(&config.address).ok()?;
364 let mut line: String = encode_client(&ClientMessage::Scratch { git_dir: git_dir.to_path_buf(), path: path.to_path_buf() });
365 line.push('\n');
366 connection.write_all(line.as_bytes()).ok()?;
367 return Some(ScratchWatch { _connection: connection });
368}
369
370pub fn wake(config: &ClientConfig, git_dir: &std::path::Path, review_key: Option<&ReviewKey>, notices: &mut dyn Write) -> Result<(), ClientFailure> {
383 let mut line: String = encode_client(&ClientMessage::Work { git_dir: git_dir.to_path_buf(), review_key: review_key.cloned() });
384 line.push('\n');
385 let mut announced: bool = false;
386 let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced, &mut ReachTrace::default())?;
387 if reader.get_mut().write_all(line.as_bytes()).is_err() {
388 return Err(ClientFailure::Unavailable);
389 }
390 while read_message(&mut reader).is_some() {}
392 return Ok(());
393}
394
395#[cfg(test)]
397#[path = "service_client_tests.rs"]
398mod tests;