Read and change a machine's files from the backend
The first half of EXPLORER.md: server/src/files.rs, which lists a directory, reads a file, writes one, and creates a file or a directory on whichever machine a setup names. Each operation is one small POSIX script run through `Transport`, the way the import listing and the usage fetch already ask a machine a question, so the local and the ssh case are one implementation rather than two that drift. The path crosses as a positional argument and never as script text; `PATH_PRELUDE` is the one line that gives a leading `~` its meaning, because a shell expands a tilde in text and not in an argument, and it is the far machine's home that has to answer. A read has four answers -- text, binary, tooBig, or the machine's own error -- because a binary file drawn as text and a big one cut off silently are both wrong in ways the reader cannot see. A write carries the sha256 the read reported and is refused with a 409 when the file has moved on, which is what happens whenever an agent is editing the file somebody is reading. `Transport::capture_with_input` is the one description of "run this there, with this on stdin", and `ship_attachment` moves onto it rather than assembling a second ssh invocation of its own. It is also the only capture that hands back the exit status, which is how the write says "this is not the file you read" without that answer looking like a failure. Exercised on both transports against the sandbox -- ssh to this VM with a throwaway key, since the quoting and the stdin path are what that proves -- including a filename with an apostrophe, one with a tab, an unreadable file, a binary one, one over the limit, and the 409. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f6bee1b8a5
commit
cc7e4f63ef
8 files changed
+1008
-46
No files matched your search
+12
-19
@@ -18,7 +18,7 @@
|
||||
//! `config.ron` on the backend, which is exactly the authority the phone
|
||||
//! is not being given.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::{DriverKind, ProviderConfig};
|
||||
use crate::session::transport::{Launch, Transport};
|
||||
@@ -186,26 +186,19 @@ pub fn shorten_home(path: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a launch to completion and returns its stdout.
|
||||
/// Runs a launch to completion and returns its stdout as text.
|
||||
///
|
||||
/// The common case of [`Transport::capture_with_input`]: nothing on stdin,
|
||||
/// a failure reported as the machine's own words (ssh's "Permission
|
||||
/// denied" or "Could not resolve hostname" is the useful half of why a
|
||||
/// setup cannot be reached), and the output read as text because every
|
||||
/// caller here is asking a question whose answer is words.
|
||||
impl Transport {
|
||||
pub async fn capture(&self, launch: &Launch) -> Result<String> {
|
||||
let child = self.spawn(launch, super::session::transport::Streams::Piped)?;
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.context("waiting for the probe to finish")?;
|
||||
if !output.status.success() {
|
||||
// ssh's own failures land on stderr -- "Permission denied",
|
||||
// "Could not resolve hostname" -- and are the useful half of
|
||||
// why a setup cannot be reached, so they are what comes back.
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
anyhow::bail!(if stderr.is_empty() {
|
||||
format!("couldn't reach it ({})", output.status)
|
||||
} else {
|
||||
stderr
|
||||
});
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
let captured = self
|
||||
.capture_with_input(launch, super::session::transport::Input::None)
|
||||
.await?;
|
||||
Ok(String::from_utf8_lossy(&captured.ok()?).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user