Skip to content

Commit 0610971

Browse files
authored
Merge pull request #746 from cgwalters/block-cmd-cleanup
A few misc cleanups
2 parents 94e5bc4 + ecf9363 commit 0610971

File tree

9 files changed

+182
-167
lines changed

9 files changed

+182
-167
lines changed

lib/src/blockdev.rs

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use fn_error_context::context;
1010
use regex::Regex;
1111
use serde::Deserialize;
1212

13+
use crate::cmdutils::CommandRunExt;
1314
use crate::install::run_in_host_mountns;
1415
use crate::task::Task;
1516

@@ -92,35 +93,21 @@ pub(crate) fn wipefs(dev: &Utf8Path) -> Result<()> {
9293
)
9394
}
9495

95-
fn list_impl(dev: Option<&Utf8Path>) -> Result<Vec<Device>> {
96-
let o = Command::new("lsblk")
96+
#[context("Listing device {dev}")]
97+
pub(crate) fn list_dev(dev: &Utf8Path) -> Result<Device> {
98+
let mut devs: DevicesOutput = Command::new("lsblk")
9799
.args(["-J", "-b", "-O"])
98-
.args(dev)
99-
.output()?;
100-
if !o.status.success() {
101-
return Err(anyhow::anyhow!("Failed to list block devices"));
102-
}
103-
let mut devs: DevicesOutput = serde_json::from_reader(&*o.stdout)?;
100+
.arg(dev)
101+
.run_and_parse_json()?;
104102
for dev in devs.blockdevices.iter_mut() {
105103
dev.backfill_missing()?;
106104
}
107-
Ok(devs.blockdevices)
108-
}
109-
110-
#[context("Listing device {dev}")]
111-
pub(crate) fn list_dev(dev: &Utf8Path) -> Result<Device> {
112-
let devices = list_impl(Some(dev))?;
113-
devices
105+
devs.blockdevices
114106
.into_iter()
115107
.next()
116108
.ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
117109
}
118110

119-
#[allow(dead_code)]
120-
pub(crate) fn list() -> Result<Vec<Device>> {
121-
list_impl(None)
122-
}
123-
124111
#[derive(Debug, Deserialize)]
125112
struct SfDiskOutput {
126113
partitiontable: PartitionTable,

lib/src/cmdutils.rs

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

lib/src/image.rs

Lines changed: 2 additions & 2 deletions
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";
@@ -22,7 +22,7 @@ pub(crate) async fn list_entrypoint() -> Result<()> {
2222
for image in images {
2323
println!("{image}");
2424
}
25-
println!("");
25+
println!();
2626

2727
println!("# Logically bound images");
2828
let mut listcmd = sysroot.imgstore.new_image_cmd()?;

lib/src/imgstorage.rs

Lines changed: 4 additions & 5 deletions
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.
@@ -126,10 +126,9 @@ impl Storage {
126126
}
127127

128128
fn init_globals() -> Result<()> {
129-
// Ensure our global storage alias dirs exist
130-
for d in [STORAGE_ALIAS_DIR] {
131-
std::fs::create_dir_all(d).with_context(|| format!("Creating {d}"))?;
132-
}
129+
// Ensure our global storage alias dir exists
130+
std::fs::create_dir_all(STORAGE_ALIAS_DIR)
131+
.with_context(|| format!("Creating {STORAGE_ALIAS_DIR}"))?;
133132
Ok(())
134133
}
135134

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/mount.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
//! Helpers for interacting with mountpoints
22
3-
use anyhow::{anyhow, Context, Result};
3+
use std::process::Command;
4+
5+
use anyhow::{anyhow, Result};
46
use camino::Utf8Path;
57
use fn_error_context::context;
68
use serde::Deserialize;
79

8-
use crate::task::Task;
10+
use crate::{cmdutils::CommandRunExt, task::Task};
911

1012
#[derive(Deserialize, Debug)]
1113
#[serde(rename_all = "kebab-case")]
@@ -26,8 +28,7 @@ pub(crate) struct Findmnt {
2628
}
2729

2830
fn run_findmnt(args: &[&str], path: &str) -> Result<Filesystem> {
29-
let desc = format!("Inspecting {path}");
30-
let o = Task::new(desc, "findmnt")
31+
let o: Findmnt = Command::new("findmnt")
3132
.args([
3233
"-J",
3334
"-v",
@@ -36,9 +37,7 @@ fn run_findmnt(args: &[&str], path: &str) -> Result<Filesystem> {
3637
])
3738
.args(args)
3839
.arg(path)
39-
.quiet()
40-
.read()?;
41-
let o: Findmnt = serde_json::from_str(&o).context("Parsing findmnt output")?;
40+
.run_and_parse_json()?;
4241
o.filesystems
4342
.into_iter()
4443
.next()

lib/src/podman.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ use camino::Utf8Path;
33
use cap_std_ext::cap_std::fs::Dir;
44
use serde::Deserialize;
55

6-
use crate::task::Task;
7-
86
/// Where we look inside our container to find our own image
97
/// for use with `bootc install`.
108
pub(crate) const CONTAINER_STORAGE: &str = "/var/lib/containers";
@@ -26,12 +24,10 @@ pub(crate) struct ImageListEntry {
2624
/// Given an image ID, return its manifest digest
2725
#[cfg(feature = "install")]
2826
pub(crate) fn imageid_to_digest(imgid: &str) -> Result<String> {
29-
use crate::install::run_in_host_mountns;
30-
let out = Task::new_cmd("podman inspect", run_in_host_mountns("podman"))
27+
use crate::cmdutils::CommandRunExt;
28+
let o: Vec<Inspect> = crate::install::run_in_host_mountns("podman")
3129
.args(["inspect", imgid])
32-
.quiet()
33-
.read()?;
34-
let o: Vec<Inspect> = serde_json::from_str(&out)?;
30+
.run_and_parse_json()?;
3531
let i = o
3632
.into_iter()
3733
.next()

0 commit comments

Comments
 (0)