1use super::service_messages::{ClientFailure, Platform, STARTING_NOTICE, UPDATING_NOTICE, position_notice};
17use super::service_protocol::{
18 ClientMessage, CommitRequest, FailureCode, LandedCapture, PROTOCOL_VERSION, RELEASE, ServiceMessage, decode_service, encode_client, encode_retire,
19};
20use super::service_review_key::ReviewKey;
21use super::service_server::Bind;
22use std::io::{BufRead, BufReader, Write};
23use std::os::unix::net::UnixStream;
24use std::time::{Duration, Instant};
25
26pub const START_WAIT: Duration = Duration::from_secs(10);
28
29pub const START_NOTICE_GRACE: Duration = Duration::from_secs(1);
31
32pub const COMMIT_WAIT: Duration = Duration::from_secs(10 * 60);
34
35pub const RECONNECTS: usize = 5;
37
38const CONNECT_PAUSE: Duration = Duration::from_millis(25);
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum Activation {
44 Managed,
46 Unsupported(Platform),
48}
49
50#[derive(Clone, Debug)]
52pub struct ClientConfig {
53 pub address: Bind,
55 pub activation: Activation,
57 pub start_wait: Duration,
59 pub commit_wait: Duration,
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
65pub enum Submitted {
66 Landed { commit: String, already: bool },
68 PrepareAgain { base: String, landed: Vec<LandedCapture> },
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, Option<String>), 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, release }) = read_message(&mut reader) {
115 let _widened = reader.get_ref().set_read_timeout(Some(config.commit_wait));
116 return Ok((reader, version, release));
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
135fn reach_current(config: &ClientConfig, notices: &mut dyn Write, announced: &mut bool) -> Result<BufReader<UnixStream>, ClientFailure> {
151 let mut give_up_at: Option<Instant> = None;
152 loop {
153 let (mut reader, version, release) = reach(config, notices, announced)?;
154 if version == PROTOCOL_VERSION && release.as_deref() == Some(RELEASE) {
155 return Ok(reader);
156 }
157 if version > PROTOCOL_VERSION {
158 return Err(ClientFailure::NoCommonVersion);
159 }
160 let deadline: Instant = *give_up_at.get_or_insert_with(|| {
161 let _written = writeln!(notices, "{UPDATING_NOTICE}");
162 return Instant::now() + config.start_wait;
163 });
164 if Instant::now() >= deadline {
165 return Err(ClientFailure::OtherRelease);
166 }
167 let mut retire: String = encode_retire(version);
169 retire.push('\n');
170 let _sent = reader.get_mut().write_all(retire.as_bytes());
171 std::thread::sleep(CONNECT_PAUSE * 4);
172 }
173}
174
175pub fn failure_for(code: FailureCode, request: &CommitRequest) -> ClientFailure {
177 let repository: String = request.repository.display().to_string();
178 return match code {
179 FailureCode::RepositoryFull => ClientFailure::RepositoryFull(repository),
180 FailureCode::RepositoryReadOnly => ClientFailure::RepositoryReadOnly(repository),
181 FailureCode::IndexInUse => ClientFailure::IndexInUse(String::from("Another program")),
182 FailureCode::Rejected => ClientFailure::Refused,
183 FailureCode::BranchKeepsChanging => ClientFailure::BranchKeepsChanging {
184 branch: request.reference.trim_start_matches("refs/heads/").to_string(),
185 times: super::service_land::LANDING_ATTEMPTS,
186 },
187 FailureCode::BranchMoved => ClientFailure::BranchMoved { branch: request.reference.trim_start_matches("refs/heads/").to_string() },
188 FailureCode::Unavailable => ClientFailure::Unavailable,
189 };
190}
191
192pub fn submit(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write) -> Result<Submitted, ClientFailure> {
203 let line: String = encode_client(&ClientMessage::Commit(Box::new(request.clone())));
204 let waiting_since: Instant = Instant::now();
205 let mut announced: bool = false;
206 let mut breaks: usize = 0;
207 let mut last_position: Option<usize> = None;
208 let mut holder: Option<String> = None;
209 loop {
210 if waiting_since.elapsed() >= config.commit_wait {
211 return Err(ClientFailure::WaitedTooLong { minutes: config.commit_wait.as_secs() / 60 });
212 }
213 let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced)?;
214 let mut payload: String = line.clone();
215 payload.push('\n');
216 if reader.get_mut().write_all(payload.as_bytes()).is_err() {
217 breaks += 1;
218 if breaks > RECONNECTS {
219 return Err(ClientFailure::MayNotHaveLanded);
220 }
221 continue;
222 }
223 loop {
224 match read_message(&mut reader) {
225 Some(ServiceMessage::Position { number, of }) => {
226 if last_position != Some(number) {
227 last_position = Some(number);
228 let _written = writeln!(notices, "{}", position_notice(number, of));
229 }
230 }
231 Some(ServiceMessage::Landed { commit, already, .. }) => return Ok(Submitted::Landed { commit, already }),
232 Some(ServiceMessage::RetryPrepare { base, landed, .. }) => return Ok(Submitted::PrepareAgain { base, landed }),
233 Some(ServiceMessage::Failed { code: FailureCode::BranchMoved, .. }) => return Ok(Submitted::Moved),
234 Some(ServiceMessage::Failed { code: FailureCode::IndexInUse, .. }) => {
235 return Err(ClientFailure::IndexInUse(holder.take().unwrap_or_else(|| { return String::from("Another program"); })));
236 }
237 Some(ServiceMessage::Failed { code, .. }) => return Err(failure_for(code, request)),
238 Some(ServiceMessage::Waiting { text }) => {
239 let _written = write!(notices, "{text}");
240 }
241 Some(ServiceMessage::Holder { who }) => holder = Some(who),
242 Some(ServiceMessage::Welcome { .. }) => {}
243 None => break,
244 }
245 }
246 breaks += 1;
248 if breaks > RECONNECTS {
249 return Err(ClientFailure::MayNotHaveLanded);
250 }
251 std::thread::sleep(CONNECT_PAUSE * u32::try_from(breaks).unwrap_or(1));
252 }
253}
254
255pub fn ask_to_retire(socket: &std::path::Path) {
267 let Ok(stream) = UnixStream::connect(socket) else { return };
268 let _timeout = stream.set_read_timeout(Some(START_WAIT));
269 let mut reader: BufReader<UnixStream> = BufReader::new(stream);
270 let Some(ServiceMessage::Welcome { version, .. }) = read_message(&mut reader) else { return };
271 let mut retire: String = encode_retire(version);
272 retire.push('\n');
273 let _sent = reader.get_mut().write_all(retire.as_bytes());
274}
275
276pub struct ScratchWatch {
279 _connection: UnixStream,
281}
282
283pub fn watch_scratch(config: &ClientConfig, git_dir: &std::path::Path, path: &std::path::Path) -> Option<ScratchWatch> {
298 let mut connection: UnixStream = connect(&config.address).ok()?;
299 let mut line: String = encode_client(&ClientMessage::Scratch { git_dir: git_dir.to_path_buf(), path: path.to_path_buf() });
300 line.push('\n');
301 connection.write_all(line.as_bytes()).ok()?;
302 return Some(ScratchWatch { _connection: connection });
303}
304
305pub fn wake(config: &ClientConfig, git_dir: &std::path::Path, review_key: Option<&ReviewKey>, notices: &mut dyn Write) -> Result<(), ClientFailure> {
318 let mut line: String = encode_client(&ClientMessage::Work { git_dir: git_dir.to_path_buf(), review_key: review_key.cloned() });
319 line.push('\n');
320 let mut announced: bool = false;
321 let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced)?;
322 if reader.get_mut().write_all(line.as_bytes()).is_err() {
323 return Err(ClientFailure::Unavailable);
324 }
325 while read_message(&mut reader).is_some() {}
327 return Ok(());
328}