use super::service_messages::{ClientFailure, Platform, STARTING_NOTICE, UPDATING_NOTICE, position_notice};
use super::service_protocol::{
ClientMessage, CommitRequest, FailureCode, PROTOCOL_VERSION, ServiceMessage, decode_service, encode_client,
};
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;
pub const REPLACEMENTS: usize = 2;
const CONNECT_PAUSE: Duration = Duration::from_millis(25);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Activation {
Managed,
NotInstalled,
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 },
}
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), 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 }) = read_message(&mut reader) {
let _widened = reader.get_ref().set_read_timeout(Some(config.commit_wait));
return Ok((reader, version));
}
}
Err(_) => match config.activation {
Activation::NotInstalled => return Err(ClientFailure::NotInstalled),
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);
}
}
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::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 replacements: usize = 0;
let mut breaks: usize = 0;
let mut last_position: Option<usize> = None;
loop {
if waiting_since.elapsed() >= config.commit_wait {
return Err(ClientFailure::WaitedTooLong { minutes: config.commit_wait.as_secs() / 60 });
}
let (mut reader, version) = reach(config, notices, &mut announced)?;
if version != PROTOCOL_VERSION {
if version > PROTOCOL_VERSION || replacements >= REPLACEMENTS {
return Err(ClientFailure::NoCommonVersion);
}
replacements += 1;
let _written = writeln!(notices, "{UPDATING_NOTICE}");
let mut retire: String = encode_client(&ClientMessage::Retire);
retire.push('\n');
let _sent = reader.get_mut().write_all(retire.as_bytes());
std::thread::sleep(CONNECT_PAUSE * 4);
continue;
}
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, .. }) => return Ok(Submitted::PrepareAgain { base }),
Some(ServiceMessage::Failed { code, .. }) => return Err(failure_for(code, request)),
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));
}
}