1#![forbid(unsafe_code)]
7
8pub mod process;
9mod send;
10pub mod shell;
11
12pub use pipette_protocol::PIPETTE_PORT;
13pub use pipette_protocol::PIPETTE_READY_MARKER;
14
15use crate::send::PipetteSender;
16use anyhow::Context;
17use futures::AsyncBufReadExt;
18use futures::AsyncRead;
19use futures::AsyncWrite;
20use futures::AsyncWriteExt;
21use futures::FutureExt as _;
22use futures::StreamExt;
23use futures::io::BufReader;
24use futures_concurrency::future::TryJoin;
25use mesh::CancelContext;
26use mesh::error::RemoteError;
27use mesh::payload::Timestamp;
28use mesh::rpc::RpcError;
29use mesh_remote::PointToPointMesh;
30use pal_async::task::Spawn;
31use pal_async::task::Task;
32use pipette_protocol::DiagnosticFile;
33use pipette_protocol::PipetteBootstrap;
34use pipette_protocol::PipetteRequest;
35use pipette_protocol::ReadFileRequest;
36use pipette_protocol::WriteFileRequest;
37use shell::UnixShell;
38use shell::WindowsShell;
39use std::path::Path;
40use std::path::PathBuf;
41use std::time::Duration;
42
43pub struct PipetteClient {
45 send: PipetteSender,
46 watch: mesh::OneshotReceiver<()>,
47 _mesh: PointToPointMesh,
48 _log_task: Task<()>,
49 _diag_task: Task<()>,
50}
51
52const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
55
56impl PipetteClient {
57 pub async fn new(
62 spawner: impl Spawn,
63 conn: impl 'static + AsyncRead + AsyncWrite + Send + Unpin,
64 output_dir: &Path,
65 ) -> anyhow::Result<Self> {
66 let mut ctx = CancelContext::new().with_timeout(CONNECT_TIMEOUT);
67 match ctx
68 .until_cancelled(Self::connect(spawner, conn, output_dir))
69 .await
70 {
71 Ok(result) => result,
72 Err(_) => Err(anyhow::anyhow!(
73 "timed out establishing pipette connection after {CONNECT_TIMEOUT:?}"
74 )),
75 }
76 }
77
78 async fn connect(
79 spawner: impl Spawn,
80 conn: impl 'static + AsyncRead + AsyncWrite + Send + Unpin,
81 output_dir: &Path,
82 ) -> anyhow::Result<Self> {
83 let (bootstrap_send, bootstrap_recv) = mesh::oneshot::<PipetteBootstrap>();
84 let mesh = PointToPointMesh::new(&spawner, conn, bootstrap_send.into());
85 let bootstrap = bootstrap_recv
86 .await
87 .context("failed to receive pipette bootstrap")?;
88
89 let PipetteBootstrap {
90 requests,
91 diag_file_recv,
92 watch,
93 log,
94 } = bootstrap;
95
96 let log_task = spawner.spawn("pipette-log", replay_logs(log));
97 let diag_task = spawner.spawn(
98 "diagnostics-recv",
99 recv_diag_files(output_dir.to_owned(), diag_file_recv),
100 );
101
102 let client = Self {
103 send: PipetteSender::new(requests),
104 watch,
105 _mesh: mesh,
106 _log_task: log_task,
107 _diag_task: diag_task,
108 };
109
110 client
118 .ping()
119 .await
120 .context("pipette liveness ping failed")?;
121
122 Ok(client)
123 }
124
125 pub async fn ping(&self) -> Result<(), RpcError> {
127 self.send.call(PipetteRequest::Ping, ()).await
128 }
129
130 pub fn windows_shell(&self) -> WindowsShell<'_> {
132 WindowsShell::new(self)
133 }
134
135 pub fn unix_shell(&self) -> UnixShell<'_> {
137 UnixShell::new(self)
138 }
139
140 pub async fn mount(
142 &self,
143 source: &str,
144 target: &str,
145 fstype: &str,
146 flags: u64,
147 mkdir_target: bool,
148 ) -> anyhow::Result<()> {
149 self.send
150 .call_failable(
151 PipetteRequest::Mount,
152 pipette_protocol::MountRequest {
153 source: source.to_owned(),
154 target: target.to_owned(),
155 fstype: fstype.to_owned(),
156 flags,
157 mkdir_target,
158 },
159 )
160 .await
161 .context("failed to send mount request")?;
162 Ok(())
163 }
164
165 pub async fn prepare_chroot(&self, target: &str) -> anyhow::Result<()> {
168 const MS_BIND: u64 = 0x1000;
170 for dir in ["/proc", "/dev", "/sys"] {
171 let mount_target = format!("{target}{dir}");
172 self.mount(dir, &mount_target, "", MS_BIND, true).await?;
173 }
174 let tmp_target = format!("{target}/tmp");
176 self.mount("tmpfs", &tmp_target, "tmpfs", 0, true).await?;
177 Ok(())
178 }
179
180 pub fn command(&self, program: impl AsRef<str>) -> process::Command<'_> {
186 process::Command::new(self, program)
187 }
188
189 pub async fn power_off(&self) -> anyhow::Result<()> {
191 self.shutdown(pipette_protocol::ShutdownType::PowerOff)
192 .await
193 }
194
195 pub async fn reboot(&self) -> anyhow::Result<()> {
197 self.shutdown(pipette_protocol::ShutdownType::Reboot).await
198 }
199
200 async fn shutdown(&self, shutdown_type: pipette_protocol::ShutdownType) -> anyhow::Result<()> {
201 tracing::debug!(?shutdown_type, "sending shutdown request to guest");
202 let r = self.send.call(
203 PipetteRequest::Shutdown,
204 pipette_protocol::ShutdownRequest { shutdown_type },
205 );
206 match r.await {
207 Ok(r) => r
208 .map_err(anyhow::Error::from)
209 .context("failed to shut down")?,
210 Err(_) => {
211 }
214 }
215 Ok(())
216 }
217
218 pub async fn read_file(&self, path: impl AsRef<str>) -> anyhow::Result<Vec<u8>> {
220 let (recv_pipe, send_pipe) = mesh::pipe::pipe();
221 let req = ReadFileRequest {
222 path: path.as_ref().to_string(),
223 sender: send_pipe,
224 };
225
226 let request_future = self.send.call_failable(PipetteRequest::ReadFile, req);
227
228 let mut contents = Vec::new();
229 let transfer_future = async { futures::io::copy(recv_pipe, &mut contents).await };
230
231 tracing::debug!(path = path.as_ref(), "beginning file read transfer");
232 let (bytes_read, io_result) = (request_future, transfer_future.map(Ok))
233 .try_join()
234 .await
235 .context("failed to read file")?;
236
237 io_result.context("io failure")?;
238 if bytes_read != contents.len() as u64 {
239 anyhow::bail!("file truncated");
240 }
241
242 tracing::debug!("file read complete");
243 Ok(contents)
244 }
245
246 pub async fn write_file(
251 &self,
252 path: impl AsRef<str>,
253 contents: impl AsyncRead,
254 ) -> anyhow::Result<()> {
255 let (recv_pipe, mut send_pipe) = mesh::pipe::pipe();
256 let req = WriteFileRequest {
257 path: path.as_ref().to_string(),
258 receiver: recv_pipe,
259 };
260
261 let request_future = self.send.call_failable(PipetteRequest::WriteFile, req);
262
263 let transfer_future = async {
264 let copy_result = futures::io::copy(contents, &mut send_pipe).await;
265 send_pipe.close().await?;
266 copy_result
267 };
268
269 tracing::debug!(path = path.as_ref(), "beginning file wurite transfer");
270 let (bytes_written, io_result) = (request_future, transfer_future.map(Ok))
271 .try_join()
272 .await
273 .context("failed to write file")?;
274 if bytes_written != io_result.context("io failure")? {
275 anyhow::bail!("file truncated");
276 }
277
278 tracing::debug!("file write complete");
279 Ok(())
280 }
281
282 pub async fn wait(self) -> Result<(), mesh::RecvError> {
284 self.watch.await
285 }
286
287 pub async fn get_time(&self) -> anyhow::Result<Timestamp> {
289 self.send
290 .call(PipetteRequest::GetTime, ())
291 .await
292 .context("failed to get time")
293 }
294
295 pub async fn crash(&self) -> Result<(), RemoteError> {
297 Self::handle_crash_result(self.send.call_failable(PipetteRequest::Crash, ()).await)
298 }
299
300 pub async fn kernel_crash(&self) -> Result<(), RemoteError> {
302 Self::handle_crash_result(
303 self.send
304 .call_failable(PipetteRequest::KernelCrash, ())
305 .await,
306 )
307 }
308
309 fn handle_crash_result(r: Result<(), RpcError<RemoteError>>) -> Result<(), RemoteError> {
310 match r {
311 Ok(()) => unreachable!(),
312 Err(RpcError::Call(err)) => Err(err),
313 Err(RpcError::Channel(_)) => {
314 Ok(())
317 }
318 }
319 }
320}
321
322async fn replay_logs(log: mesh::pipe::ReadPipe) {
323 let mut lines = BufReader::new(log).lines();
324 while let Some(line) = lines.next().await {
325 match line {
326 Ok(line) => tracing::debug!(target: "pipette", "{}", line),
327 Err(err) => {
328 tracing::error!(
329 error = &err as &dyn std::error::Error,
330 "pipette log failure"
331 );
332 break;
333 }
334 }
335 }
336}
337
338async fn recv_diag_files(output_dir: PathBuf, mut diag_file_recv: mesh::Receiver<DiagnosticFile>) {
339 while let Some(diag_file) = diag_file_recv.next().await {
340 let DiagnosticFile { name, mut receiver } = diag_file;
341 tracing::debug!(name, "receiving diagnostic file");
342 let path = output_dir.join(&name);
343 let file = fs_err::File::create(&path).expect("failed to create diagnostic file {name}");
344 futures::io::copy(&mut receiver, &mut futures::io::AllowStdIo::new(file))
345 .await
346 .expect("failed to write diagnostic file");
347 tracing::debug!(name, "diagnostic file transfer complete");
348
349 #[expect(
350 clippy::disallowed_methods,
351 reason = "ATTACHMENT is most reliable when using true canonicalized paths"
352 )]
353 let canonical_path = path
354 .canonicalize()
355 .expect("failed to canonicalize attachment path");
356 println!("[[ATTACHMENT|{}]]", canonical_path.display());
358 }
359}