Skip to content

Commit 4ac9079

Browse files
committed
lib: Move Command extensions to new mod cmdutil
Prep for more stuff there. Signed-off-by: Colin Walters <[email protected]>
1 parent 691910d commit 4ac9079

File tree

6 files changed

+136
-128
lines changed

6 files changed

+136
-128
lines changed

lib/src/cmdutils.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use std::{
2+
io::{Read, Seek},
3+
process::Command,
4+
};
5+
6+
use anyhow::Result;
7+
8+
/// Helpers intended for [`std::process::Command`].
9+
pub(crate) trait CommandRunExt {
10+
fn run(&mut self) -> Result<()>;
11+
}
12+
13+
/// Helpers intended for [`std::process::ExitStatus`].
14+
pub(crate) trait ExitStatusExt {
15+
/// If the exit status signals it was not successful, return an error.
16+
/// Note that we intentionally *don't* include the command string
17+
/// in the output; we leave it to the caller to add that if they want,
18+
/// as it may be verbose.
19+
fn check_status(&mut self, stderr: std::fs::File) -> Result<()>;
20+
}
21+
22+
/// Parse the last chunk (e.g. 1024 bytes) from the provided file,
23+
/// ensure it's UTF-8, and return that value. This function is infallible;
24+
/// if the file cannot be read for some reason, a copy of a static string
25+
/// is returned.
26+
fn last_utf8_content_from_file(mut f: std::fs::File) -> String {
27+
// u16 since we truncate to just the trailing bytes here
28+
// to avoid pathological error messages
29+
const MAX_STDERR_BYTES: u16 = 1024;
30+
let size = f
31+
.metadata()
32+
.map_err(|e| {
33+
tracing::warn!("failed to fstat: {e}");
34+
})
35+
.map(|m| m.len().try_into().unwrap_or(u16::MAX))
36+
.unwrap_or(0);
37+
let size = size.min(MAX_STDERR_BYTES);
38+
let seek_offset = -(size as i32);
39+
let mut stderr_buf = Vec::with_capacity(size.into());
40+
// We should never fail to seek()+read() really, but let's be conservative
41+
let r = match f
42+
.seek(std::io::SeekFrom::End(seek_offset.into()))
43+
.and_then(|_| f.read_to_end(&mut stderr_buf))
44+
{
45+
Ok(_) => String::from_utf8_lossy(&stderr_buf),
46+
Err(e) => {
47+
tracing::warn!("failed seek+read: {e}");
48+
"<failed to read stderr>".into()
49+
}
50+
};
51+
(&*r).to_owned()
52+
}
53+
54+
impl ExitStatusExt for std::process::ExitStatus {
55+
fn check_status(&mut self, stderr: std::fs::File) -> Result<()> {
56+
let stderr_buf = last_utf8_content_from_file(stderr);
57+
if self.success() {
58+
return Ok(());
59+
}
60+
anyhow::bail!(format!("Subprocess failed: {self:?}\n{stderr_buf}"))
61+
}
62+
}
63+
64+
impl CommandRunExt for Command {
65+
/// Synchronously execute the child, and return an error if the child exited unsuccessfully.
66+
fn run(&mut self) -> Result<()> {
67+
let stderr = tempfile::tempfile()?;
68+
self.stderr(stderr.try_clone()?);
69+
self.status()?.check_status(stderr)
70+
}
71+
}
72+
73+
/// Helpers intended for [`tokio::process::Command`].
74+
#[allow(dead_code)]
75+
pub(crate) trait AsyncCommandRunExt {
76+
async fn run(&mut self) -> Result<()>;
77+
}
78+
79+
impl AsyncCommandRunExt for tokio::process::Command {
80+
/// Asynchronously execute the child, and return an error if the child exited unsuccessfully.
81+
///
82+
async fn run(&mut self) -> Result<()> {
83+
let stderr = tempfile::tempfile()?;
84+
self.stderr(stderr.try_clone()?);
85+
self.status().await?.check_status(stderr)
86+
}
87+
}
88+
89+
#[test]
90+
fn command_run_ext() {
91+
// The basics
92+
Command::new("true").run().unwrap();
93+
assert!(Command::new("false").run().is_err());
94+
95+
// Verify we capture stderr
96+
let e = Command::new("/bin/sh")
97+
.args(["-c", "echo expected-this-oops-message 1>&2; exit 1"])
98+
.run()
99+
.err()
100+
.unwrap();
101+
similar_asserts::assert_eq!(
102+
e.to_string(),
103+
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected-this-oops-message\n"
104+
);
105+
106+
// Ignoring invalid UTF-8
107+
let e = Command::new("/bin/sh")
108+
.args([
109+
"-c",
110+
r"echo -e 'expected\xf5\x80\x80\x80\x80-foo\xc0bar\xc0\xc0' 1>&2; exit 1",
111+
])
112+
.run()
113+
.err()
114+
.unwrap();
115+
similar_asserts::assert_eq!(
116+
e.to_string(),
117+
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected�����-foo�bar��\n"
118+
);
119+
}
120+
121+
#[tokio::test]
122+
async fn async_command_run_ext() {
123+
use tokio::process::Command as AsyncCommand;
124+
let mut success = AsyncCommand::new("true");
125+
let mut fail = AsyncCommand::new("false");
126+
// Run these in parallel just because we can
127+
let (success, fail) = tokio::join!(success.run(), fail.run(),);
128+
success.unwrap();
129+
assert!(fail.is_err());
130+
}

lib/src/image.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use anyhow::{Context, Result};
66
use fn_error_context::context;
77
use ostree_ext::container::{ImageReference, Transport};
88

9-
use crate::{imgstorage::Storage, utils::CommandRunExt};
9+
use crate::{cmdutils::CommandRunExt, imgstorage::Storage};
1010

1111
/// The name of the image we push to containers-storage if nothing is specified.
1212
const IMAGE_DEFAULT: &str = "localhost/bootc";

lib/src/imgstorage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use fn_error_context::context;
2222
use std::os::fd::OwnedFd;
2323
use tokio::process::Command as AsyncCommand;
2424

25-
use crate::utils::{AsyncCommandRunExt, CommandRunExt, ExitStatusExt};
25+
use crate::cmdutils::{AsyncCommandRunExt, CommandRunExt, ExitStatusExt};
2626

2727
// Pass only 100 args at a time just to avoid potentially overflowing argument
2828
// vectors; not that this should happen in reality, but just in case.

lib/src/install.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ use rustix::fs::{FileTypeExt, MetadataExt as _};
4343
use serde::{Deserialize, Serialize};
4444

4545
use self::baseline::InstallBlockDeviceOpts;
46+
use crate::cmdutils::CommandRunExt;
4647
use crate::containerenv::ContainerExecutionInfo;
4748
use crate::mount::Filesystem;
4849
use crate::spec::ImageReference;
4950
use crate::store::Storage;
5051
use crate::task::Task;
51-
use crate::utils::{sigpolicy_from_opts, CommandRunExt};
52+
use crate::utils::sigpolicy_from_opts;
5253

5354
/// The default "stateroot" or "osname"; see https://github.com/ostreedev/ostree/issues/2794
5455
const STATEROOT_DEFAULT: &str = "default";

lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
mod boundimage;
1212
pub mod cli;
13+
mod cmdutils;
1314
pub(crate) mod deploy;
1415
pub(crate) mod generator;
1516
mod image;

lib/src/utils.rs

Lines changed: 1 addition & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::future::Future;
2-
use std::io::{Read, Seek, Write};
2+
use std::io::Write;
33
use std::os::fd::BorrowedFd;
44
use std::process::Command;
55
use std::time::Duration;
@@ -10,87 +10,6 @@ use ostree::glib;
1010
use ostree_ext::container::SignatureSource;
1111
use ostree_ext::ostree;
1212

13-
/// Helpers intended for [`std::process::Command`].
14-
pub(crate) trait CommandRunExt {
15-
fn run(&mut self) -> Result<()>;
16-
}
17-
18-
/// Helpers intended for [`std::process::ExitStatus`].
19-
pub(crate) trait ExitStatusExt {
20-
/// If the exit status signals it was not successful, return an error.
21-
/// Note that we intentionally *don't* include the command string
22-
/// in the output; we leave it to the caller to add that if they want,
23-
/// as it may be verbose.
24-
fn check_status(&mut self, stderr: std::fs::File) -> Result<()>;
25-
}
26-
27-
/// Parse the last chunk (e.g. 1024 bytes) from the provided file,
28-
/// ensure it's UTF-8, and return that value. This function is infallible;
29-
/// if the file cannot be read for some reason, a copy of a static string
30-
/// is returned.
31-
fn last_utf8_content_from_file(mut f: std::fs::File) -> String {
32-
// u16 since we truncate to just the trailing bytes here
33-
// to avoid pathological error messages
34-
const MAX_STDERR_BYTES: u16 = 1024;
35-
let size = f
36-
.metadata()
37-
.map_err(|e| {
38-
tracing::warn!("failed to fstat: {e}");
39-
})
40-
.map(|m| m.len().try_into().unwrap_or(u16::MAX))
41-
.unwrap_or(0);
42-
let size = size.min(MAX_STDERR_BYTES);
43-
let seek_offset = -(size as i32);
44-
let mut stderr_buf = Vec::with_capacity(size.into());
45-
// We should never fail to seek()+read() really, but let's be conservative
46-
let r = match f
47-
.seek(std::io::SeekFrom::End(seek_offset.into()))
48-
.and_then(|_| f.read_to_end(&mut stderr_buf))
49-
{
50-
Ok(_) => String::from_utf8_lossy(&stderr_buf),
51-
Err(e) => {
52-
tracing::warn!("failed seek+read: {e}");
53-
"<failed to read stderr>".into()
54-
}
55-
};
56-
(&*r).to_owned()
57-
}
58-
59-
impl ExitStatusExt for std::process::ExitStatus {
60-
fn check_status(&mut self, stderr: std::fs::File) -> Result<()> {
61-
let stderr_buf = last_utf8_content_from_file(stderr);
62-
if self.success() {
63-
return Ok(());
64-
}
65-
anyhow::bail!(format!("Subprocess failed: {self:?}\n{stderr_buf}"))
66-
}
67-
}
68-
69-
impl CommandRunExt for Command {
70-
/// Synchronously execute the child, and return an error if the child exited unsuccessfully.
71-
fn run(&mut self) -> Result<()> {
72-
let stderr = tempfile::tempfile()?;
73-
self.stderr(stderr.try_clone()?);
74-
self.status()?.check_status(stderr)
75-
}
76-
}
77-
78-
/// Helpers intended for [`tokio::process::Command`].
79-
#[allow(dead_code)]
80-
pub(crate) trait AsyncCommandRunExt {
81-
async fn run(&mut self) -> Result<()>;
82-
}
83-
84-
impl AsyncCommandRunExt for tokio::process::Command {
85-
/// Asynchronously execute the child, and return an error if the child exited unsuccessfully.
86-
///
87-
async fn run(&mut self) -> Result<()> {
88-
let stderr = tempfile::tempfile()?;
89-
self.stderr(stderr.try_clone()?);
90-
self.status().await?.check_status(stderr)
91-
}
92-
}
93-
9413
/// Try to look for keys injected by e.g. rpm-ostree requesting machine-local
9514
/// changes; if any are present, return `true`.
9615
pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool {
@@ -271,46 +190,3 @@ fn test_sigpolicy_from_opts() {
271190
SignatureSource::ContainerPolicyAllowInsecure
272191
);
273192
}
274-
275-
#[test]
276-
fn command_run_ext() {
277-
// The basics
278-
Command::new("true").run().unwrap();
279-
assert!(Command::new("false").run().is_err());
280-
281-
// Verify we capture stderr
282-
let e = Command::new("/bin/sh")
283-
.args(["-c", "echo expected-this-oops-message 1>&2; exit 1"])
284-
.run()
285-
.err()
286-
.unwrap();
287-
similar_asserts::assert_eq!(
288-
e.to_string(),
289-
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected-this-oops-message\n"
290-
);
291-
292-
// Ignoring invalid UTF-8
293-
let e = Command::new("/bin/sh")
294-
.args([
295-
"-c",
296-
r"echo -e 'expected\xf5\x80\x80\x80\x80-foo\xc0bar\xc0\xc0' 1>&2; exit 1",
297-
])
298-
.run()
299-
.err()
300-
.unwrap();
301-
similar_asserts::assert_eq!(
302-
e.to_string(),
303-
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected�����-foo�bar��\n"
304-
);
305-
}
306-
307-
#[tokio::test]
308-
async fn async_command_run_ext() {
309-
use tokio::process::Command as AsyncCommand;
310-
let mut success = AsyncCommand::new("true");
311-
let mut fail = AsyncCommand::new("false");
312-
// Run these in parallel just because we can
313-
let (success, fail) = tokio::join!(success.run(), fail.run(),);
314-
success.unwrap();
315-
assert!(fail.is_err());
316-
}

0 commit comments

Comments
 (0)