use crate::ProcessHold;
use ambassador::Delegate;
use rsynko_manager::ChangeKind;
use rsynko_memory::{PlannedChange, RsyncEndpoint, RsyncSyntax, SyncCommand, SyncObservation};
use rsynko_rsync::*;
use std::io::{BufReader, Read};
use std::process::{Child, ChildStdout, Command, Stdio};
use std::sync::mpsc::Sender;
#[allow(
clippy::duplicated_attributes,
reason = "one delegation per capability, not per target"
)]
#[derive(Debug, Delegate)]
#[delegate(RsyncEndpointAlg, target = "syntax")]
#[delegate(RsyncEndpointViewAlg, target = "syntax")]
#[delegate(SyncCommandAlg, target = "syntax")]
#[delegate(SyncCommandViewAlg, target = "syntax")]
#[delegate(SyncChangeAlg, target = "syntax")]
#[delegate(SyncObservationAlg, target = "syntax")]
#[delegate(SyncObservationViewAlg, target = "syntax")]
pub struct ProcessSyncEnv {
syntax: RsyncSyntax,
observations: Sender<SyncObservation>,
hold: ProcessHold,
}
impl RsyncSorts for ProcessSyncEnv {
type Endpoint = RsyncEndpoint;
type Command = SyncCommand;
type Observation = SyncObservation;
type Change = PlannedChange;
}
#[derive(Debug)]
pub struct SyncRun {
child: Child,
output: BufReader<ChildStdout>,
}
#[derive(Debug, thiserror::Error)]
pub enum ProcessSyncError {
#[error("the transfer program could not be started: {0}")]
Unstartable(std::io::Error),
#[error("the transfer could not be read: {0}")]
Unreadable(std::io::Error),
#[error("the transfer refused: {0}")]
Refused(String),
}
impl ProcessSyncEnv {
#[must_use]
pub fn new(observations: Sender<SyncObservation>) -> Self {
Self::held(observations, ProcessHold::default())
}
#[must_use]
pub const fn held(observations: Sender<SyncObservation>, hold: ProcessHold) -> Self {
Self {
syntax: RsyncSyntax,
observations,
hold,
}
}
}
impl SyncRunAlg for ProcessSyncEnv {
type Error = ProcessSyncError;
type Run = SyncRun;
fn start_sync(&self, command: &Self::Command) -> Result<Self::Run, Self::Error> {
let mut started = Command::new(self.command_program(command));
started
.args(self.command_arguments(command).collect::<Vec<_>>())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(unix)]
std::os::unix::process::CommandExt::process_group(&mut started, 0);
let mut child = started.spawn().map_err(ProcessSyncError::Unstartable)?;
self.hold.running(Some(child.id()));
let stdout = child
.stdout
.take()
.ok_or_else(|| ProcessSyncError::Refused("the transfer wrote nothing".to_owned()))?;
Ok(SyncRun {
child,
output: BufReader::new(stdout),
})
}
fn next_sync_line(&self, run: &mut Self::Run) -> Result<Option<String>, Self::Error> {
let mut line = Vec::new();
let mut byte = [0_u8; 1];
loop {
let read = run
.output
.read(&mut byte)
.map_err(ProcessSyncError::Unreadable)?;
if read == 0 {
break;
}
if byte[0] == b'\n' || byte[0] == b'\r' {
if line.is_empty() {
continue;
}
break;
}
line.push(byte[0]);
}
if line.is_empty() {
return Ok(None);
}
Ok(Some(String::from_utf8_lossy(&line).into_owned()))
}
fn finish_sync(&self, mut run: Self::Run) -> Result<(), Self::Error> {
let status = run.child.wait().map_err(ProcessSyncError::Unreadable)?;
self.hold.running(None);
if status.success() {
return Ok(());
}
let mut refusal = String::new();
if let Some(errors) = run.child.stderr.as_mut() {
let _read = errors.read_to_string(&mut refusal);
}
let refusal = refusal.trim();
Err(ProcessSyncError::Refused(if refusal.is_empty() {
format!("the transfer ended with {status}")
} else {
refusal.lines().take(4).collect::<Vec<_>>().join("; ")
}))
}
}
impl SyncWatchAlg for ProcessSyncEnv {
fn watch_sync(&self, observation: &Self::Observation) {
let _watched = self.observations.send(observation.clone());
}
}