use super::service_messages::{ClientFailure, Platform, STARTING_NOTICE, UPDATING_NOTICE, position_notice};
use super::service_protocol::{
ClientMessage, CommitRequest, FailureCode, LandedCapture, PROTOCOL_VERSION, RELEASE, ServiceMessage, decode_service, encode_client, encode_retire,
};
use super::service_review_key::ReviewKey;
use super::service_server::Bind;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::time::{Duration, Instant};
pub const START_WAIT: Duration = Duration::from_secs(10);
pub const START_NOTICE_GRACE: Duration = Duration::from_secs(1);
pub const COMMIT_WAIT: Duration = Duration::from_secs(10 * 60);
pub const RECONNECTS: usize = 5;
const CONNECT_PAUSE: Duration = Duration::from_millis(25);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Activation {
Managed,
Unsupported(Platform),
}
#[derive(Clone, Debug)]
pub struct ClientConfig {
pub address: Bind,
pub activation: Activation,
pub start_wait: Duration,
pub commit_wait: Duration,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Submitted {
Landed { commit: String, already: bool },
PrepareAgain { base: String, landed: Vec<LandedCapture> },
Moved,
}
fn connect(address: &Bind) -> std::io::Result<UnixStream> {
return match address {
#[cfg(target_os = "linux")]
Bind::Abstract(name) => {
use std::os::linux::net::SocketAddrExt;
UnixStream::connect_addr(&std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?)
}
#[cfg(not(target_os = "linux"))]
Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
Bind::Path(path) => UnixStream::connect(path),
Bind::Inherited => Err(std::io::Error::other("a client does not inherit the socket")),
};
}
fn read_message(reader: &mut BufReader<UnixStream>) -> Option<ServiceMessage> {
let mut line: String = String::new();
return match reader.read_line(&mut line) {
Ok(read) if read > 0 => decode_service(line.trim_end_matches('\n')).ok(),
_ => None,
};
}
fn reach(config: &ClientConfig, notices: &mut dyn Write, announced: &mut bool) -> Result<(BufReader<UnixStream>, u32, Option<String>), ClientFailure> {
let started: Instant = Instant::now();
loop {
match connect(&config.address) {
Ok(stream) => {
let _timeout = stream.set_read_timeout(Some(config.start_wait));
let mut reader: BufReader<UnixStream> = BufReader::new(stream);
if let Some(ServiceMessage::Welcome { version, release }) = read_message(&mut reader) {
let _widened = reader.get_ref().set_read_timeout(Some(config.commit_wait));
return Ok((reader, version, release));
}
}
Err(_) => match config.activation {
Activation::Unsupported(platform) => return Err(ClientFailure::NoActivation(platform)),
Activation::Managed => {}
},
}
if started.elapsed() >= config.start_wait {
return Err(ClientFailure::DidNotStart);
}
if !*announced && started.elapsed() >= START_NOTICE_GRACE {
*announced = true;
let _written = writeln!(notices, "{STARTING_NOTICE}");
}
std::thread::sleep(CONNECT_PAUSE);
}
}
fn reach_current(config: &ClientConfig, notices: &mut dyn Write, announced: &mut bool) -> Result<BufReader<UnixStream>, ClientFailure> {
let mut give_up_at: Option<Instant> = None;
loop {
let (mut reader, version, release) = reach(config, notices, announced)?;
if version == PROTOCOL_VERSION && release.as_deref() == Some(RELEASE) {
return Ok(reader);
}
if version > PROTOCOL_VERSION {
return Err(ClientFailure::NoCommonVersion);
}
let deadline: Instant = *give_up_at.get_or_insert_with(|| {
let _written = writeln!(notices, "{UPDATING_NOTICE}");
return Instant::now() + config.start_wait;
});
if Instant::now() >= deadline {
return Err(ClientFailure::OtherRelease);
}
let mut retire: String = encode_retire(version);
retire.push('\n');
let _sent = reader.get_mut().write_all(retire.as_bytes());
std::thread::sleep(CONNECT_PAUSE * 4);
}
}
pub fn failure_for(code: FailureCode, request: &CommitRequest) -> ClientFailure {
let repository: String = request.repository.display().to_string();
return match code {
FailureCode::RepositoryFull => ClientFailure::RepositoryFull(repository),
FailureCode::RepositoryReadOnly => ClientFailure::RepositoryReadOnly(repository),
FailureCode::IndexInUse => ClientFailure::IndexInUse(String::from("Another program")),
FailureCode::Rejected => ClientFailure::Refused,
FailureCode::BranchKeepsChanging => ClientFailure::BranchKeepsChanging {
branch: request.reference.trim_start_matches("refs/heads/").to_string(),
times: super::service_land::LANDING_ATTEMPTS,
},
FailureCode::BranchMoved => ClientFailure::BranchMoved { branch: request.reference.trim_start_matches("refs/heads/").to_string() },
FailureCode::Unavailable => ClientFailure::Unavailable,
};
}
pub fn submit(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write) -> Result<Submitted, ClientFailure> {
let line: String = encode_client(&ClientMessage::Commit(Box::new(request.clone())));
let waiting_since: Instant = Instant::now();
let mut announced: bool = false;
let mut breaks: usize = 0;
let mut last_position: Option<usize> = None;
let mut holder: Option<String> = None;
loop {
if waiting_since.elapsed() >= config.commit_wait {
return Err(ClientFailure::WaitedTooLong { minutes: config.commit_wait.as_secs() / 60 });
}
let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced)?;
let mut payload: String = line.clone();
payload.push('\n');
if reader.get_mut().write_all(payload.as_bytes()).is_err() {
breaks += 1;
if breaks > RECONNECTS {
return Err(ClientFailure::MayNotHaveLanded);
}
continue;
}
loop {
match read_message(&mut reader) {
Some(ServiceMessage::Position { number, of }) => {
if last_position != Some(number) {
last_position = Some(number);
let _written = writeln!(notices, "{}", position_notice(number, of));
}
}
Some(ServiceMessage::Landed { commit, already, .. }) => return Ok(Submitted::Landed { commit, already }),
Some(ServiceMessage::RetryPrepare { base, landed, .. }) => return Ok(Submitted::PrepareAgain { base, landed }),
Some(ServiceMessage::Failed { code: FailureCode::BranchMoved, .. }) => return Ok(Submitted::Moved),
Some(ServiceMessage::Failed { code: FailureCode::IndexInUse, .. }) => {
return Err(ClientFailure::IndexInUse(holder.take().unwrap_or_else(|| { return String::from("Another program"); })));
}
Some(ServiceMessage::Failed { code, .. }) => return Err(failure_for(code, request)),
Some(ServiceMessage::Waiting { text }) => {
let _written = write!(notices, "{text}");
}
Some(ServiceMessage::Holder { who }) => holder = Some(who),
Some(ServiceMessage::Welcome { .. }) => {}
None => break,
}
}
breaks += 1;
if breaks > RECONNECTS {
return Err(ClientFailure::MayNotHaveLanded);
}
std::thread::sleep(CONNECT_PAUSE * u32::try_from(breaks).unwrap_or(1));
}
}
pub fn wake(config: &ClientConfig, common_dir: &std::path::Path, review_key: Option<&ReviewKey>, notices: &mut dyn Write) -> Result<(), ClientFailure> {
let mut line: String = encode_client(&ClientMessage::Work { common_dir: common_dir.to_path_buf(), review_key: review_key.cloned() });
line.push('\n');
let mut announced: bool = false;
let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced)?;
if reader.get_mut().write_all(line.as_bytes()).is_err() {
return Err(ClientFailure::Unavailable);
}
while read_message(&mut reader).is_some() {}
return Ok(());
}