Skip to content

Commit b730dab

Browse files
committed
cmd: migrate away from failure
1 parent 2db06c1 commit b730dab

File tree

8 files changed

+137
-49
lines changed

8 files changed

+137
-49
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55

66
## Unreleased
77

8+
### Added
9+
10+
- New variant `CommandError::ExecutionFailed`
11+
- New variant `CommandError::KillAfterTimeoutFailed`
12+
- New variant `CommandError::SandboxImagePullFailed`
13+
- New variant `CommandError::SandboxImageMissing`
14+
- New variant `CommandError::WorkspaceNotMountedCorrectly`
15+
- New variant `CommandError::InvalidDockerInspectOutput`
16+
- New variant `CommandError::IO`
17+
- New struct `KillFailedError`
18+
819
### Changed
920

1021
- **BREAKING**: support for CI toolchains is now gated behind the
1122
`unstable-toolchain-ci` Cargo feature.
23+
- **BREAKING**: all functions and methods inside `cmd` now return `CommandError`.
1224
- `winapi` is no longer required on unix; `nix` is no longer required on windows.
1325
- Relaxed lifetime restrictions of `Build::cmd` and `Build::cargo`.
1426

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fs2 = "0.4.3"
3737
remove_dir_all = "0.5.2"
3838
base64 = "0.12.0"
3939
getrandom = { version = "0.1.12", features = ["std"] }
40+
thiserror = "1.0.20"
4041

4142
[target.'cfg(unix)'.dependencies]
4243
nix = "0.11.0"

src/cmd/mod.rs

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ pub use sandbox::*;
88

99
use crate::native;
1010
use crate::workspace::Workspace;
11-
use failure::{Error, Fail};
1211
use futures_util::{
1312
future::{self, FutureExt},
1413
stream::{self, TryStreamExt},
@@ -58,20 +57,79 @@ pub(crate) mod container_dirs {
5857
}
5958

6059
/// Error happened while executing a command.
61-
#[derive(Debug, Fail)]
60+
#[derive(Debug, thiserror::Error)]
6261
#[non_exhaustive]
6362
pub enum CommandError {
6463
/// The command didn't output anything to stdout or stderr for more than the timeout, and it
6564
/// was killed. The timeout's value (in seconds) is the first value.
66-
#[fail(display = "no output for {} seconds", _0)]
65+
#[error("no output for {0} seconds")]
6766
NoOutputFor(u64),
67+
6868
/// The command took more time than the timeout to end, and it was killed. The timeout's value
6969
/// (in seconds) is the first value.
70-
#[fail(display = "command timed out after {} seconds", _0)]
70+
#[error("command timed out after {0} seconds")]
7171
Timeout(u64),
72+
73+
/// The command failed to execute.
74+
#[error("command failed: {0}")]
75+
ExecutionFailed(ExitStatus),
76+
77+
/// Killing the underlying process after the timeout failed.
78+
#[error("{0}")]
79+
KillAfterTimeoutFailed(#[source] KillFailedError),
80+
7281
/// The sandbox ran out of memory and was killed.
73-
#[fail(display = "container ran out of memory")]
82+
#[error("container ran out of memory")]
7483
SandboxOOM,
84+
85+
/// Pulling a sandbox image from the registry failed
86+
#[error("failed to pull the sandbox image from the registry: {0}")]
87+
SandboxImagePullFailed(#[source] Box<CommandError>),
88+
89+
/// The sandbox image is missing from the local system.
90+
#[error("sandbox image missing from the local system: {0}")]
91+
SandboxImageMissing(#[source] Box<CommandError>),
92+
93+
/// Running rustwide inside a Docker container requires the workspace directory to be mounted
94+
/// from the host system. This error happens if that's not true, for example if the workspace
95+
/// lives in a directory inside the container.
96+
#[error("the workspace is not mounted from outside the container")]
97+
WorkspaceNotMountedCorrectly,
98+
99+
/// The data received from the `docker inspect` command is not valid.
100+
#[error("invalid output of `docker inspect`: {0}")]
101+
InvalidDockerInspectOutput(#[source] serde_json::Error),
102+
103+
/// An I/O error occured while executing the command.
104+
#[error(transparent)]
105+
IO(#[from] std::io::Error),
106+
}
107+
108+
/// Error happened while trying to kill a process.
109+
#[derive(Debug, thiserror::Error)]
110+
#[cfg_attr(unix, error(
111+
"failed to kill the process with PID {pid}{}",
112+
.errno.map(|e| format!(": {}", e.desc())).unwrap_or_else(String::new)
113+
))]
114+
#[cfg_attr(not(unix), error("failed to kill the process with PID {pid}"))]
115+
pub struct KillFailedError {
116+
pub(crate) pid: u32,
117+
#[cfg(unix)]
118+
pub(crate) errno: Option<nix::errno::Errno>,
119+
}
120+
121+
impl KillFailedError {
122+
/// Return the PID of the process that couldn't be killed.
123+
pub fn pid(&self) -> u32 {
124+
self.pid
125+
}
126+
127+
/// Return the underlying error number provided by the operative system.
128+
#[cfg(any(unix, doc))]
129+
#[cfg_attr(docs_rs, doc(cfg(unix)))]
130+
pub fn errno(&self) -> Option<i32> {
131+
self.errno.map(|errno| errno as i32)
132+
}
75133
}
76134

77135
/// Name and kind of a binary executed by [`Command`](struct.Command.html).
@@ -285,7 +343,7 @@ impl<'w, 'pl> Command<'w, 'pl> {
285343

286344
/// Run the prepared command and return an error if it fails (for example with a non-zero exit
287345
/// code or a timeout).
288-
pub fn run(self) -> Result<(), Error> {
346+
pub fn run(self) -> Result<(), CommandError> {
289347
self.run_inner(false)?;
290348
Ok(())
291349
}
@@ -296,11 +354,11 @@ impl<'w, 'pl> Command<'w, 'pl> {
296354
/// Even though the output will be captured and returned, if output logging is enabled (as it
297355
/// is by default) the output will be also logged. You can disable this behavior by calling the
298356
/// [`log_output`](struct.Command.html#method.log_output) method.
299-
pub fn run_capture(self) -> Result<ProcessOutput, Error> {
357+
pub fn run_capture(self) -> Result<ProcessOutput, CommandError> {
300358
Ok(self.run_inner(true)?)
301359
}
302360

303-
fn run_inner(self, capture: bool) -> Result<ProcessOutput, Error> {
361+
fn run_inner(self, capture: bool) -> Result<ProcessOutput, CommandError> {
304362
if let Some(mut builder) = self.sandbox {
305363
let workspace = self
306364
.workspace
@@ -438,7 +496,7 @@ impl<'w, 'pl> Command<'w, 'pl> {
438496
if out.status.success() {
439497
Ok(out.into())
440498
} else {
441-
failure::bail!("command `{}` failed", cmdstr);
499+
Err(CommandError::ExecutionFailed(out.status))
442500
}
443501
}
444502
}
@@ -499,7 +557,7 @@ async fn log_command(
499557
timeout: Option<Duration>,
500558
no_output_timeout: Option<Duration>,
501559
log_output: bool,
502-
) -> Result<InnerProcessOutput, Error> {
560+
) -> Result<InnerProcessOutput, CommandError> {
503561
let timeout = if let Some(t) = timeout {
504562
t
505563
} else {
@@ -532,12 +590,12 @@ async fn log_command(
532590
.map(move |result| match result {
533591
// If the timeout elapses, kill the process
534592
Err(_timeout) => Err(match native::kill_process(child_id) {
535-
Ok(()) => Error::from(CommandError::NoOutputFor(no_output_timeout.as_secs())),
536-
Err(err) => err,
593+
Ok(()) => CommandError::NoOutputFor(no_output_timeout.as_secs()),
594+
Err(err) => CommandError::KillAfterTimeoutFailed(err),
537595
}),
538596

539597
// If an error occurred reading the line, flatten the error
540-
Ok((_, Err(read_err))) => Err(Error::from(read_err)),
598+
Ok((_, Err(read_err))) => Err(read_err.into()),
541599

542600
// If the read was successful, return the `OutputKind` and the read line
543601
Ok((out_kind, Ok(line))) => Ok((out_kind, line)),
@@ -546,7 +604,7 @@ async fn log_command(
546604
// If the process is in a tight output loop the timeout on the process might fail to
547605
// be executed, so this extra check prevents the process to run without limits.
548606
if start.elapsed() > timeout {
549-
return future::err(Error::from(CommandError::Timeout(timeout.as_secs())));
607+
return future::err(CommandError::Timeout(timeout.as_secs()));
550608
}
551609

552610
if let Some(f) = &mut process_lines {
@@ -587,12 +645,12 @@ async fn log_command(
587645
match result {
588646
// If the timeout elapses, kill the process
589647
Err(_timeout) => Err(match native::kill_process(child_id) {
590-
Ok(()) => Error::from(CommandError::Timeout(timeout.as_secs())),
591-
Err(err) => err,
648+
Ok(()) => CommandError::Timeout(timeout.as_secs()),
649+
Err(err) => CommandError::KillAfterTimeoutFailed(err),
592650
}),
593651

594652
// If an error occurred with the child
595-
Ok(Err(err)) => Err(Error::from(err)),
653+
Ok(Err(err)) => Err(err.into()),
596654

597655
// If the read was successful, return the process's exit status
598656
Ok(Ok(exit_status)) => Ok(exit_status),

src/cmd/sandbox.rs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::cmd::{Command, CommandError, ProcessLinesActions, ProcessOutput};
22
use crate::Workspace;
3-
use failure::Error;
43
use log::{error, info};
54
use serde::Deserialize;
5+
use std::error::Error;
66
use std::fmt;
77
use std::path::{Path, PathBuf};
88
use std::time::Duration;
@@ -16,7 +16,7 @@ impl SandboxImage {
1616
/// Load a local image present in the host machine.
1717
///
1818
/// If the image is not available locally an error will be returned instead.
19-
pub fn local(name: &str) -> Result<Self, Error> {
19+
pub fn local(name: &str) -> Result<Self, CommandError> {
2020
let image = SandboxImage { name: name.into() };
2121
info!("sandbox image is local, skipping pull");
2222
image.ensure_exists_locally()?;
@@ -27,12 +27,13 @@ impl SandboxImage {
2727
///
2828
/// This will access the network to download the image from the registry. If pulling fails an
2929
/// error will be returned instead.
30-
pub fn remote(name: &str) -> Result<Self, Error> {
30+
pub fn remote(name: &str) -> Result<Self, CommandError> {
3131
let mut image = SandboxImage { name: name.into() };
3232
info!("pulling image {} from Docker Hub", name);
3333
Command::new_workspaceless("docker")
3434
.args(&["pull", &name])
35-
.run()?;
35+
.run()
36+
.map_err(|e| CommandError::SandboxImagePullFailed(Box::new(e)))?;
3637
if let Some(name_with_hash) = image.get_name_with_hash() {
3738
image.name = name_with_hash;
3839
info!("pulled image {}", image.name);
@@ -41,12 +42,13 @@ impl SandboxImage {
4142
Ok(image)
4243
}
4344

44-
fn ensure_exists_locally(&self) -> Result<(), Error> {
45+
fn ensure_exists_locally(&self) -> Result<(), CommandError> {
4546
info!("checking the image {} is available locally", self.name);
4647
Command::new_workspaceless("docker")
4748
.args(&["image", "inspect", &self.name])
4849
.log_output(false)
49-
.run()?;
50+
.run()
51+
.map_err(|e| CommandError::SandboxImageMissing(Box::new(e)))?;
5052
Ok(())
5153
}
5254

@@ -85,7 +87,7 @@ struct MountConfig {
8587
}
8688

8789
impl MountConfig {
88-
fn host_path(&self, workspace: &Workspace) -> Result<PathBuf, Error> {
90+
fn host_path(&self, workspace: &Workspace) -> Result<PathBuf, CommandError> {
8991
if let Some(container) = workspace.current_container() {
9092
// If we're inside a Docker container we'll need to remap the mount sources to point to
9193
// the directories in the host system instead of the containers. To do that we try to
@@ -97,13 +99,13 @@ impl MountConfig {
9799
return Ok(Path::new(mount.source()).join(shared));
98100
}
99101
}
100-
failure::bail!("the workspace is not mounted from outside the container");
102+
Err(CommandError::WorkspaceNotMountedCorrectly)
101103
} else {
102104
Ok(crate::utils::normalize_path(&self.host_path))
103105
}
104106
}
105107

106-
fn to_volume_arg(&self, workspace: &Workspace) -> Result<String, Error> {
108+
fn to_volume_arg(&self, workspace: &Workspace) -> Result<String, CommandError> {
107109
let perm = match self.perm {
108110
MountKind::ReadWrite => "rw",
109111
MountKind::ReadOnly => "ro",
@@ -116,7 +118,7 @@ impl MountConfig {
116118
))
117119
}
118120

119-
fn to_mount_arg(&self, workspace: &Workspace) -> Result<String, Error> {
121+
fn to_mount_arg(&self, workspace: &Workspace) -> Result<String, CommandError> {
120122
let mut opts_with_leading_comma = vec![];
121123

122124
if self.perm == MountKind::ReadOnly {
@@ -214,7 +216,7 @@ impl SandboxBuilder {
214216
self
215217
}
216218

217-
fn create(self, workspace: &Workspace) -> Result<Container<'_>, Error> {
219+
fn create(self, workspace: &Workspace) -> Result<Container<'_>, CommandError> {
218220
let mut args: Vec<String> = vec!["create".into()];
219221

220222
for mount in &self.mounts {
@@ -285,16 +287,18 @@ impl SandboxBuilder {
285287
log_output: bool,
286288
log_command: bool,
287289
capture: bool,
288-
) -> Result<ProcessOutput, Error> {
290+
) -> Result<ProcessOutput, CommandError> {
289291
let container = self.create(workspace)?;
290292

291293
// Ensure the container is properly deleted even if something panics
292294
scopeguard::defer! {{
293295
if let Err(err) = container.delete() {
294296
error!("failed to delete container {}", container.id);
295297
error!("caused by: {}", err);
296-
for cause in err.iter_causes() {
298+
let mut err: &dyn Error = &err;
299+
while let Some(cause) = err.source() {
297300
error!("caused by: {}", cause);
301+
err = cause;
298302
}
299303
}
300304
}}
@@ -336,14 +340,15 @@ impl fmt::Display for Container<'_> {
336340
}
337341

338342
impl Container<'_> {
339-
fn inspect(&self) -> Result<InspectContainer, Error> {
343+
fn inspect(&self) -> Result<InspectContainer, CommandError> {
340344
let output = Command::new(self.workspace, "docker")
341345
.args(&["inspect", &self.id])
342346
.log_output(false)
343347
.run_capture()?;
344348

345349
let mut data: Vec<InspectContainer> =
346-
::serde_json::from_str(&output.stdout_lines().join("\n"))?;
350+
::serde_json::from_str(&output.stdout_lines().join("\n"))
351+
.map_err(CommandError::InvalidDockerInspectOutput)?;
347352
assert_eq!(data.len(), 1);
348353
Ok(data.pop().unwrap())
349354
}
@@ -356,7 +361,7 @@ impl Container<'_> {
356361
log_output: bool,
357362
log_command: bool,
358363
capture: bool,
359-
) -> Result<ProcessOutput, Error> {
364+
) -> Result<ProcessOutput, CommandError> {
360365
let mut cmd = Command::new(self.workspace, "docker")
361366
.args(&["start", "-a", &self.id])
362367
.timeout(timeout)
@@ -373,17 +378,16 @@ impl Container<'_> {
373378

374379
// Return a different error if the container was killed due to an OOM
375380
if details.state.oom_killed {
376-
if let Err(err) = res {
377-
Err(err.context(CommandError::SandboxOOM).into())
378-
} else {
379-
Err(CommandError::SandboxOOM.into())
380-
}
381+
Err(match res {
382+
Ok(_) | Err(CommandError::ExecutionFailed(_)) => CommandError::SandboxOOM,
383+
Err(err) => err,
384+
})
381385
} else {
382386
res
383387
}
384388
}
385389

386-
fn delete(&self) -> Result<(), Error> {
390+
fn delete(&self) -> Result<(), CommandError> {
387391
Command::new(self.workspace, "docker")
388392
.args(&["rm", "-f", &self.id])
389393
.run()

src/native/unix.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::cmd::KillFailedError;
12
use failure::Error;
23
use nix::{
34
sys::signal::{kill, Signal},
@@ -9,9 +10,18 @@ use std::path::Path;
910

1011
const EXECUTABLE_BITS: u32 = 0o5;
1112

12-
pub(crate) fn kill_process(id: u32) -> Result<(), Error> {
13-
kill(Pid::from_raw(id as i32), Signal::SIGKILL)?;
14-
Ok(())
13+
pub(crate) fn kill_process(id: u32) -> Result<(), KillFailedError> {
14+
match kill(Pid::from_raw(id as i32), Signal::SIGKILL) {
15+
Ok(()) => Ok(()),
16+
Err(err) => Err(KillFailedError {
17+
pid: id,
18+
errno: if let nix::Error::Sys(errno) = err {
19+
Some(errno)
20+
} else {
21+
None
22+
},
23+
}),
24+
}
1525
}
1626

1727
pub(crate) fn current_user() -> Option<u32> {

0 commit comments

Comments
 (0)