mod error;
pub use error::{ReadFailureKind, ReadStage, RemoteReadError};
#[cfg(unix)]
mod command;
#[cfg(unix)]
mod session;
#[cfg(unix)]
mod stderr;
#[cfg(unix)]
mod wire;
use super::address::RemoteFile;
use strop_core::worker::CancelToken;
use strop_core::Buffer;
pub fn read(file: &RemoteFile, token: &CancelToken) -> Result<Buffer, RemoteReadError> {
#[cfg(not(unix))]
{
let _ = (file, token);
return Err(RemoteReadError::bare(
ReadStage::Spawn,
ReadFailureKind::Unsupported,
"remote reads require Unix process supervision",
));
}
#[cfg(unix)]
unix::read(file, token)
}
#[cfg(unix)]
mod unix {
use super::super::address::RemoteFile;
use super::error::{Fault, ReadFailureKind, ReadStage, RemoteReadError};
use super::{command, session, stderr, Buffer, CancelToken};
use std::time::{Duration, Instant};
use strop_core::process::OwnedProcess;
use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
use tokio::runtime::Runtime;
use tokio::task::JoinHandle;
const REAP_GRACE: Duration = Duration::from_secs(5);
const STDERR_DRAIN: Duration = Duration::from_secs(1);
const POLL: Duration = Duration::from_millis(20);
pub(super) fn read(file: &RemoteFile, token: &CancelToken) -> Result<Buffer, RemoteReadError> {
if token.is_cancelled() {
return Err(RemoteReadError::bare(
ReadStage::Spawn,
ReadFailureKind::Cancelled,
"the read was cancelled before the ssh client started",
));
}
let remote = file.to_string();
let mut command = command::sftp_subsystem(file);
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(&mut 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(read_failure(&runtime, child, log, None, fault, &remote)),
};
match runtime.block_on(session::run(stdin, stdout, file.path(), token)) {
Ok(text) => {
if let Err(fault) = runtime.block_on(reap_after_shutdown(&mut child)) {
return Err(read_failure(
&runtime,
child,
log,
Some(drain),
fault,
&remote,
));
}
drop(runtime);
if token.is_cancelled() {
return Err(RemoteReadError::fault(
&remote,
Fault::cancelled(ReadStage::Teardown),
));
}
let mut buffer = Buffer::from_text(&text);
buffer.readonly = true;
Ok(buffer)
}
Err(fault) => Err(read_failure(
&runtime,
child,
log,
Some(drain),
fault,
&remote,
)),
}
}
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 read_failure(
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 });
}
let mut error = RemoteReadError::fault(remote, fault)
.stderr(log.lock().render())
.exit(exit);
if error.kind() == ReadFailureKind::Connect {
error.refine_connect();
}
error
}
async fn reap_after_shutdown(child: &mut OwnedProcess) -> Result<(), Fault> {
let start = Instant::now();
loop {
let failed = |failure: strop_core::worker::Failure| {
Fault::new(ReadStage::Teardown, ReadFailureKind::Io, failure.message)
};
if child.has_exited().map_err(failed)? {
child.terminate().map_err(failed)?;
let status = child.wait().map_err(failed)?;
return if status.success() {
Ok(())
} else {
Err(Fault::new(
ReadStage::Teardown,
ReadFailureKind::Io,
format!("ssh {status}"),
))
};
}
if start.elapsed() >= REAP_GRACE {
return Err(Fault::new(
ReadStage::Teardown,
ReadFailureKind::Deadline,
"ssh did not exit after SFTP shutdown",
));
}
tokio::time::sleep(POLL).await;
}
}
}