1use std::collections::HashMap;
4use std::time::Duration;
5
6use synwire_core::BoxFuture;
7use synwire_core::vfs::error::VfsError;
8use synwire_core::vfs::grep_options::GrepOptions;
9use synwire_core::vfs::protocol::Vfs;
10use synwire_core::vfs::types::{
11 CpOptions, DirEntry, EditResult, ExecuteResponse, FileContent, GlobEntry, GrepMatch, LsOptions,
12 RmOptions, TransferResult, VfsCapabilities, WriteResult,
13};
14use tokio::process::Command;
15use tokio::time::timeout;
16
17use crate::vfs::local::LocalProvider;
18
19const MAX_OUTPUT_BYTES: usize = 1024 * 1024; pub struct Shell {
24 fs: LocalProvider,
25 env: HashMap<String, String>,
26 default_timeout: Duration,
27}
28
29impl std::fmt::Debug for Shell {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.debug_struct("Shell").finish()
32 }
33}
34
35impl Shell {
36 pub fn new(
42 root: impl Into<std::path::PathBuf>,
43 env: HashMap<String, String>,
44 timeout_secs: u64,
45 ) -> Result<Self, VfsError> {
46 Ok(Self {
47 fs: LocalProvider::new(root)?,
48 env,
49 default_timeout: Duration::from_secs(timeout_secs),
50 })
51 }
52
53 pub fn execute_cmd<'a>(
55 &'a self,
56 cmd: &'a str,
57 args: &'a [String],
58 timeout_override: Option<Duration>,
59 ) -> BoxFuture<'a, Result<ExecuteResponse, VfsError>> {
60 Box::pin(async move {
61 let deadline = timeout_override.unwrap_or(self.default_timeout);
62 let cwd = self.fs.pwd().await?;
63
64 let child = Command::new(cmd)
65 .args(args)
66 .envs(&self.env)
67 .current_dir(&cwd)
68 .output();
69
70 let output = timeout(deadline, child)
71 .await
72 .map_err(|_| VfsError::Timeout(format!("{cmd} timed out after {deadline:?}")))?
73 .map_err(VfsError::Io)?;
74
75 let stdout = truncate_string(
76 String::from_utf8_lossy(&output.stdout).into_owned(),
77 MAX_OUTPUT_BYTES,
78 );
79 let stderr = truncate_string(
80 String::from_utf8_lossy(&output.stderr).into_owned(),
81 MAX_OUTPUT_BYTES,
82 );
83
84 Ok(ExecuteResponse {
85 exit_code: output.status.code().unwrap_or(-1),
86 stdout,
87 stderr,
88 })
89 })
90 }
91}
92
93fn truncate_string(mut s: String, max: usize) -> String {
94 const SUFFIX: &str = "\n[truncated]";
95 if s.len() > max {
96 let keep = max.saturating_sub(SUFFIX.len());
97 let mut boundary = keep;
99 while boundary > 0 && !s.is_char_boundary(boundary) {
100 boundary -= 1;
101 }
102 s.truncate(boundary);
103 s.push_str(SUFFIX);
104 }
105 s
106}
107
108impl Vfs for Shell {
110 fn ls(&self, path: &str, opts: LsOptions) -> BoxFuture<'_, Result<Vec<DirEntry>, VfsError>> {
111 self.fs.ls(path, opts)
112 }
113
114 fn read(&self, path: &str) -> BoxFuture<'_, Result<FileContent, VfsError>> {
115 self.fs.read(path)
116 }
117
118 fn write(&self, path: &str, content: &[u8]) -> BoxFuture<'_, Result<WriteResult, VfsError>> {
119 self.fs.write(path, content)
120 }
121
122 fn edit(
123 &self,
124 path: &str,
125 old: &str,
126 new: &str,
127 ) -> BoxFuture<'_, Result<EditResult, VfsError>> {
128 self.fs.edit(path, old, new)
129 }
130
131 fn grep(
132 &self,
133 pattern: &str,
134 opts: GrepOptions,
135 ) -> BoxFuture<'_, Result<Vec<GrepMatch>, VfsError>> {
136 self.fs.grep(pattern, opts)
137 }
138
139 fn glob(&self, pattern: &str) -> BoxFuture<'_, Result<Vec<GlobEntry>, VfsError>> {
140 self.fs.glob(pattern)
141 }
142
143 fn upload(&self, from: &str, to: &str) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
144 self.fs.upload(from, to)
145 }
146
147 fn download(&self, from: &str, to: &str) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
148 self.fs.download(from, to)
149 }
150
151 fn pwd(&self) -> BoxFuture<'_, Result<String, VfsError>> {
152 self.fs.pwd()
153 }
154
155 fn cd(&self, path: &str) -> BoxFuture<'_, Result<(), VfsError>> {
156 self.fs.cd(path)
157 }
158
159 fn rm(&self, path: &str, opts: RmOptions) -> BoxFuture<'_, Result<(), VfsError>> {
160 self.fs.rm(path, opts)
161 }
162
163 fn cp(
164 &self,
165 from: &str,
166 to: &str,
167 opts: CpOptions,
168 ) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
169 self.fs.cp(from, to, opts)
170 }
171
172 fn mv_file(&self, from: &str, to: &str) -> BoxFuture<'_, Result<TransferResult, VfsError>> {
173 self.fs.mv_file(from, to)
174 }
175
176 fn capabilities(&self) -> VfsCapabilities {
177 VfsCapabilities::all()
178 }
179
180 fn provider_name(&self) -> &'static str {
181 "Shell"
182 }
183}