use std::collections::HashMap;
use std::time::Duration;
use synwire_core::BoxFuture;
use synwire_core::vfs::error::VfsError;
use synwire_core::vfs::grep_options::GrepOptions;
use synwire_core::vfs::protocol::Vfs;
use synwire_core::vfs::types::{
CpOptions, DirEntry, EditResult, ExecuteResponse, FileContent, GlobEntry, GrepMatch, LsOptions,
RmOptions, TransferResult, VfsCapabilities, WriteResult,
};
use tokio::process::Command;
use tokio::time::timeout;
use crate::vfs::local::LocalProvider;
const MAX_OUTPUT_BYTES: usize = 1024 * 1024;
pub struct Shell {
fs: LocalProvider,
env: HashMap<String, String>,
default_timeout: Duration,
}
impl std::fmt::Debug for Shell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Shell").finish()
}
}
impl Shell {
pub fn new(
root: impl Into<std::path::PathBuf>,
env: HashMap<String, String>,
timeout_secs: u64,
) -> Result<Self, VfsError> {
Ok(Self {
fs: LocalProvider::new(root)?,
env,
default_timeout: Duration::from_secs(timeout_secs),
})
}
pub fn execute_cmd<'a>(
&'a self,
cmd: &'a str,
args: &'a [String],
timeout_override: Option<Duration>,
) -> BoxFuture<'a, Result<ExecuteResponse, VfsError>> {
Box::pin(async move {
let deadline = timeout_override.unwrap_or(self.default_timeout);
let cwd = self.fs.pwd().await?;
let child = Command::new(cmd)
.args(args)
.envs(&self.env)
.current_dir(&cwd)
.output();
let output = timeout(deadline, child)
.await
.map_err(|_| VfsError::Timeout(format!("{cmd} timed out after {deadline:?}")))?
.map_err(VfsError::Io)?;
let stdout = truncate_string(
String::from_utf8_lossy(&output.stdout).into_owned(),
MAX_OUTPUT_BYTES,
);
let stderr = truncate_string(
String::from_utf8_lossy(&output.stderr).into_owned(),
MAX_OUTPUT_BYTES,
);
Ok(ExecuteResponse {
exit_code: output.status.code().unwrap_or(-1),
stdout,
stderr,
})
})
}
}
fn truncate_string(mut s: String, max: usize) -> String {
const SUFFIX: &str = "\n[truncated]";
if s.len() > max {
let keep = max.saturating_sub(SUFFIX.len());
let mut boundary = keep;
while boundary > 0 && !s.is_char_boundary(boundary) {
boundary -= 1;
}
s.truncate(boundary);
s.push_str(SUFFIX);
}
s
}
impl Vfs for Shell {
fn ls(&self, path: &str, opts: LsOptions) -> BoxFuture<'_, Result<Vec<DirEntry>, VfsError>> {
self.fs.ls(path, opts)
}
fn read(&self, path: &str) -> BoxFuture<'_, Result<FileContent, VfsError>> {
self.fs.read(path)
}
fn write(&self, path: &str, content: &[u8]) -> BoxFuture<'_, Result<WriteResult, VfsError>> {
self.fs.write(path, content)
}
fn edit(
&self,
path: &str,
old: &str,
new: &str,
) -> BoxFuture<'_, Result<EditResult, VfsError>> {
self.fs.edit(path, old, new)
}
fn grep(
&self,
pattern: &str,
opts: GrepOptions,
) -> BoxFuture<'_, Result<Vec<GrepMatch>, VfsError>> {
self.fs.grep(pattern, opts)
}
fn glob(&self, pattern: &str) -> BoxFuture<'_, Result<Vec<GlobEntry>, VfsError>> {
self.fs.glob(pattern)
}
fn upload(&self, from: &str, to: &str) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
self.fs.upload(from, to)
}
fn download(&self, from: &str, to: &str) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
self.fs.download(from, to)
}
fn pwd(&self) -> BoxFuture<'_, Result<String, VfsError>> {
self.fs.pwd()
}
fn cd(&self, path: &str) -> BoxFuture<'_, Result<(), VfsError>> {
self.fs.cd(path)
}
fn rm(&self, path: &str, opts: RmOptions) -> BoxFuture<'_, Result<(), VfsError>> {
self.fs.rm(path, opts)
}
fn cp(
&self,
from: &str,
to: &str,
opts: CpOptions,
) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
self.fs.cp(from, to, opts)
}
fn mv_file(&self, from: &str, to: &str) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
self.fs.mv_file(from, to)
}
fn capabilities(&self) -> VfsCapabilities {
VfsCapabilities::all()
}
fn provider_name(&self) -> &'static str {
"Shell"
}
}