use super::error::{Fault, ReadFailureKind, ReadStage, RemoteReadError};
use super::session;
use super::stderr;
use super::wire::ReadOnlySftp;
use crate::pool::StopSignal;
use std::cell::Cell;
use std::process::Command;
use std::time::Duration;
use strop_core::process::OwnedProcess;
use strop_core::worker::CancelToken;
use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
use tokio::runtime::Runtime;
use tokio::task::JoinHandle;
const STDERR_DRAIN: Duration = Duration::from_secs(1);
pub(crate) struct Physical {
pub(crate) epoch: u64,
pub(crate) runtime: Runtime,
pub(crate) child: OwnedProcess,
pub(crate) codec: ReadOnlySftp<ChildStdin, ChildStdout>,
log: stderr::SharedTail,
pub(crate) drain: Option<JoinHandle<()>>,
pub(crate) advertised: super::wire::Advertised,
}
impl Physical {
pub(crate) fn diagnostics(&self) -> Option<String> {
self.log.lock().render()
}
pub(crate) fn connect(
command: &mut Command,
token: &CancelToken,
stop: &StopSignal,
epoch: u64,
remote: &str,
) -> Result<Self, RemoteReadError> {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(error) => {
return Err(RemoteReadError::bare(
ReadStage::Spawn,
ReadFailureKind::Io,
format!("private tokio runtime: {error}"),
)
.remote(remote))
}
};
let mut child = match OwnedProcess::spawn(command, token) {
Ok(child) => child,
Err(failure) => {
let kind = if token.is_cancelled() {
ReadFailureKind::Cancelled
} else {
ReadFailureKind::Spawn
};
return Err(
RemoteReadError::bare(ReadStage::Spawn, kind, failure.message).remote(remote),
);
}
};
let log = stderr::SharedTail::default();
let pipes = {
let _context = runtime.enter();
convert(&mut child, log.clone())
};
let (stdin, stdout, drain) = match pipes {
Ok(pipes) => pipes,
Err(fault) => return Err(diagnose(runtime, child, log, None, fault, remote)),
};
let stage = Cell::new(ReadStage::Connect);
let negotiated = runtime.block_on(session::guarded(
ReadOnlySftp::connect(stdin, stdout),
token,
stop,
&stage,
));
match negotiated {
Ok((codec, advertised)) => Ok(Self {
epoch,
runtime,
child,
codec,
log,
drain: Some(drain),
advertised,
}),
Err(fault) => Err(diagnose(runtime, child, log, Some(drain), fault, remote)),
}
}
pub(crate) fn retire(mut self) -> Result<(), RemoteReadError> {
self.child.terminate().map_err(|failure| {
RemoteReadError::bare(ReadStage::Teardown, ReadFailureKind::Io, failure.message)
})?;
self.child.wait().map_err(|failure| {
RemoteReadError::bare(ReadStage::Teardown, ReadFailureKind::Io, failure.message)
})?;
if let Some(drain) = self.drain.take() {
self.runtime
.block_on(async { tokio::time::timeout(STDERR_DRAIN, drain).await })
.map_err(|error| {
RemoteReadError::bare(
ReadStage::Teardown,
ReadFailureKind::Deadline,
error.to_string(),
)
})?
.map_err(|error| {
RemoteReadError::bare(
ReadStage::Teardown,
ReadFailureKind::Io,
error.to_string(),
)
})?;
}
Ok(())
}
}
fn convert(
child: &mut OwnedProcess,
log: stderr::SharedTail,
) -> Result<(ChildStdin, ChildStdout, JoinHandle<()>), Fault> {
let missing = |what: &str| {
Fault::new(
ReadStage::Connect,
ReadFailureKind::Io,
format!("{what} pipe missing"),
)
};
let rejected = |what: &str, error: std::io::Error| {
Fault::new(
ReadStage::Connect,
ReadFailureKind::Io,
format!("{what}: {error}"),
)
};
let stderr_pipe = child.take_stderr().ok_or_else(|| missing("ssh stderr"))?;
let stderr_pipe =
ChildStderr::from_std(stderr_pipe).map_err(|error| rejected("ssh stderr", error))?;
let drain = stderr::spawn_drain(stderr_pipe, log);
let stdin_pipe = child.take_stdin().ok_or_else(|| missing("ssh stdin"))?;
let stdin = ChildStdin::from_std(stdin_pipe).map_err(|error| rejected("ssh stdin", error))?;
let stdout_pipe = child.take_stdout().ok_or_else(|| missing("ssh stdout"))?;
let stdout =
ChildStdout::from_std(stdout_pipe).map_err(|error| rejected("ssh stdout", error))?;
Ok((stdin, stdout, drain))
}
fn diagnose(
runtime: Runtime,
mut child: OwnedProcess,
log: stderr::SharedTail,
drain: Option<JoinHandle<()>>,
fault: Fault,
remote: &str,
) -> RemoteReadError {
let exited = child.has_exited().unwrap_or(false);
let signalled = child.terminate().is_ok();
let exit = if exited || signalled {
child.wait().ok().map(|status| status.to_string())
} else {
None
};
if let Some(drain) = drain {
let _ = runtime.block_on(async { tokio::time::timeout(STDERR_DRAIN, drain).await });
}
drop(child);
drop(runtime);
let mut error = RemoteReadError::fault(remote, fault)
.stderr(log.lock().render())
.exit(exit);
if error.kind() == ReadFailureKind::Connect {
error.refine_connect();
}
error
}