Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/cli/src/yas_fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ async fn cmd_write(
create_parents: parents,
mode,
content,
in_place: false,
},
json,
)
Expand Down Expand Up @@ -1241,6 +1242,7 @@ mod tests {
create_parents: false,
mode: 0,
content: b"contents".to_vec(),
in_place: false,
},
ApplyItem::Mkdir {
path: wire_path("directory").unwrap(),
Expand Down
59 changes: 37 additions & 22 deletions crates/cli/tests/client_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -888,28 +888,38 @@ async fn files_answer_as_the_os_does() {
pair("ENOENT", "lstat")
);

// In place through a symlink: the same inode, the new bytes.
// In place through a symlink: the same inode, the new bytes. Inline-sized content goes in
// one APPLY (CAPABILITY_APPLY_IN_PLACE), bigger content is staged: both answer alike.
use std::os::unix::fs::MetadataExt;
let inode = std::fs::metadata(base.join("file")).unwrap().ino();
root.write_in_place("to-file", b"new content")
.await
.unwrap();
assert_eq!(std::fs::read(base.join("file")).unwrap(), b"new content");
assert_eq!(std::fs::metadata(base.join("file")).unwrap().ino(), inode);
assert!(
std::fs::symlink_metadata(base.join("to-file"))
.unwrap()
.file_type()
.is_symlink()
);
assert_eq!(
named(root.write_in_place("dir", b"x").await.unwrap_err()),
pair("EISDIR", "open")
);
assert_eq!(
named(root.write_in_place("missing/file", b"x").await.unwrap_err()),
pair("ENOENT", "open")
);
let staged = vec![b's'; yas_client::wire::fs::MAX_INLINE_BYTES + 1];
for content in [&b"new content"[..], &staged] {
let written = root.write_in_place("to-file", content).await.unwrap();
assert_eq!(written.hash, *blake3::hash(content).as_bytes());
assert_eq!(std::fs::read(base.join("file")).unwrap(), content);
assert_eq!(std::fs::metadata(base.join("file")).unwrap().ino(), inode);
assert!(
std::fs::symlink_metadata(base.join("to-file"))
.unwrap()
.file_type()
.is_symlink()
);
let error = root.write_in_place("dir", content).await.unwrap_err();
assert!(error.is_conflict(), "{error:?}");
assert_eq!(named(error), pair("EISDIR", "open"));
assert_eq!(
named(
root.write_in_place("missing/file", content)
.await
.unwrap_err()
),
pair("ENOENT", "open")
);
// A new file is made where it is named.
let _ = std::fs::remove_file(base.join("fresh"));
root.write_in_place("fresh", content).await.unwrap();
assert_eq!(std::fs::read(base.join("fresh")).unwrap(), content);
}

assert_eq!(
named(root.read("missing").await.unwrap_err()),
Expand Down Expand Up @@ -1465,12 +1475,17 @@ exec sleep 600"#;
std::fs::canonicalize(String::from_utf8(cwd).unwrap()).unwrap(),
std::fs::canonicalize(directory.path()).unwrap()
);
// Nothing else starts: waiting for the next command finds none.
// Nothing else starts: waiting for the next command times out (TIMEOUT
// since #54; NOT_FOUND is for an exited terminal or an evicted index).
let waited = client
.wait_terminal_command(id, None, Duration::from_millis(200))
.await
.unwrap_err();
assert!(waited.is_not_found(), "{waited:?}");
assert_eq!(
waited.status(),
Some(yas_client::wire::core::Status::Timeout),
"{waited:?}"
);
client.close_terminal(id).await.unwrap();
}

Expand Down
37 changes: 32 additions & 5 deletions crates/client/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ impl Client {
.and_then(|limits| wire::Limits::from_extensions(&limits).ok())
.map_or(0, |limits| limits.capabilities)
}

/// The most content one inline FS write carries on this server (`MAX_INLINE_BYTES`, as it
/// advertises it): 0 before the FS family is negotiated.
fn fs_inline_bytes(&self) -> usize {
self.family_limits(family::FS)
.and_then(|limits| wire::Limits::from_extensions(&limits).ok())
.map_or(0, |limits| {
(limits.max_inline_bytes as usize).min(wire::MAX_INLINE_BYTES)
})
}
}

/// File content read by [`FsRoot::read`] and friends.
Expand Down Expand Up @@ -797,6 +807,7 @@ impl FsRoot {
create_parents: options.create_parents,
mode: options.mode,
content: content.to_vec(),
in_place: false,
})
.await;
}
Expand All @@ -819,18 +830,34 @@ impl FsRoot {
.await
}

/// Write a whole file as `open(2)` with `O_WRONLY|O_CREAT|O_TRUNC` and `write(2)` would
/// (`STAGE_IN_PLACE`), as Node's `writeFile` does: through a final symlink, an existing
/// file keeping its inode, owner and mode, a new one mode 0666 less the server's umask. Not
/// atomic: a failure can leave the file truncated. Needs `CAPABILITY_STAGE_IN_PLACE`; a
/// failure carries its [`os_error`].
/// Write a whole file as `open(2)` with `O_WRONLY|O_CREAT|O_TRUNC` and `write(2)` would,
/// as Node's `writeFile` does: through a final symlink, an existing file keeping its inode,
/// owner and mode, a new one mode 0666 less the server's umask. Not atomic: a failure can
/// leave the file truncated. Content up to the server's inline limit goes in one `APPLY`
/// (one round trip) where it offers `CAPABILITY_APPLY_IN_PLACE`; other content is staged
/// (`STAGE_IN_PLACE`, needing `CAPABILITY_STAGE_IN_PLACE`; two round trips). A failure
/// carries its [`os_error`] either way.
pub async fn write_in_place(&self, path: &str, content: &[u8]) -> Result<Written> {
if content.len() as u64 > MAX_FILE_BYTES {
return Err(Error::invalid(format!(
"file is {} bytes; the YAS limit is {MAX_FILE_BYTES}",
content.len()
)));
}
if content.len() <= self.client.fs_inline_bytes()
&& self.client.fs_capabilities() & schema::CAPABILITY_APPLY_IN_PLACE as u32 != 0
{
return self
.apply_one(ApplyItem::WriteInline {
path: wire_path(path)?,
precondition: Precondition::Any,
create_parents: false,
mode: 0,
content: content.to_vec(),
in_place: true,
})
.await;
}
self.stage_and_commit(
wire_path(path)?,
content,
Expand Down
3 changes: 2 additions & 1 deletion crates/client/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,8 @@ impl Client {
/// one with this journal `index`, or the one running now (else the next
/// one to start). Answers its journal record (exit code, times, command
/// line). When it still runs at the timeout: a `Timeout` error; when
/// none started by then (or it left the backlog): a `NotFound` status.
/// none started by then: a `Timeout` status; when it left the backlog,
/// or the terminal exited: a `NotFound` status.
/// Needs shell integration.
pub async fn wait_terminal_command(
&self,
Expand Down
Loading
Loading