Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

std::process::ExitStatus Into Result, or .expect(), or something #73125

Closed
ijackson opened this issue Jun 8, 2020 · 1 comment · Fixed by #82973
Closed

std::process::ExitStatus Into Result, or .expect(), or something #73125

ijackson opened this issue Jun 8, 2020 · 1 comment · Fixed by #82973
Labels
C-feature-request Category: A feature request, i.e: not implemented / a PR. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@ijackson
Copy link
Contributor

ijackson commented Jun 8, 2020

It is not particularly convenient to use the return value from wait() in std::process. Perhaps it should be convertable to a Result ? Or gain its own expect methods etc. ?

@jonas-schievink jonas-schievink added C-feature-request Category: A feature request, i.e: not implemented / a PR. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Jun 8, 2020
JohnTitor added a commit to JohnTitor/rust that referenced this issue Jan 14, 2021
Add missing methods to unix ExitStatusExt

These are the methods corresponding to the remaining exit status examination macros from `wait.h`.  `WCOREDUMP` isn't in SuS but is it is very standard.  I have not done portability testing to see if this builds everywhere, so I may need to Do Something if it doesn't.

There is also a bugfix and doc improvement to `.signal()`, and an `.into_raw()` accessor.

This would fix rust-lang#73128 and fix rust-lang#73129.  Please let me know if you like this direction, and if so I will open the tracking issue and so on.

If this MR goes well, I may tackle rust-lang#73125 next - I have an idea for how to do it.
m-ou-se added a commit to m-ou-se/rust that referenced this issue Jan 14, 2021
Add missing methods to unix ExitStatusExt

These are the methods corresponding to the remaining exit status examination macros from `wait.h`.  `WCOREDUMP` isn't in SuS but is it is very standard.  I have not done portability testing to see if this builds everywhere, so I may need to Do Something if it doesn't.

There is also a bugfix and doc improvement to `.signal()`, and an `.into_raw()` accessor.

This would fix rust-lang#73128 and fix rust-lang#73129.  Please let me know if you like this direction, and if so I will open the tracking issue and so on.

If this MR goes well, I may tackle rust-lang#73125 next - I have an idea for how to do it.
m-ou-se added a commit to m-ou-se/rust that referenced this issue Jan 14, 2021
Add missing methods to unix ExitStatusExt

These are the methods corresponding to the remaining exit status examination macros from `wait.h`.  `WCOREDUMP` isn't in SuS but is it is very standard.  I have not done portability testing to see if this builds everywhere, so I may need to Do Something if it doesn't.

There is also a bugfix and doc improvement to `.signal()`, and an `.into_raw()` accessor.

This would fix rust-lang#73128 and fix rust-lang#73129.  Please let me know if you like this direction, and if so I will open the tracking issue and so on.

If this MR goes well, I may tackle rust-lang#73125 next - I have an idea for how to do it.
@ijackson
Copy link
Contributor Author

ijackson commented Feb 1, 2021

@KodrAus apropos of recent discussion in #81452 and #73131:

Motivation

It is far too easy to accidentally drop an ExitStatus on the floor, rather than checking it properly. Indeed almost none of the examples in std::process get this right !

Doing it right with the current API is highly un-ergonomic. We should be using Result.

Furthermore, there is no code in the Rust stdlib which will generate an appropriate message describing the fate of a subprocess. (I.e., generate a message comparable to that from the shell naming a fatal signal, or a message quoting the value passed to exit.)

Proposal sketch

impl ExitStatus {
    pub fn exit_ok(self) -> Result<(), ExitStatusError>;
}

#[derive(Copy,Clone,Debug,Eq,PartialEq)]
/// Subprocess status other than success.
pub struct ExitStatusError {
    wait_status: NonZeroU32 // on Unix; something else on Windows
}

impl std::error::Error for ExitStatusError { }
impl From<ExitStatusError> for ExitStatus {...}
impl ExitStatusExt for ExitStatusError { ... same as on ExitStatus ... }

impl Display for ExitStatusError {
   ... checks ExitStatusExt::signal(), ::coredump() etc.
}

impl ExitStatusError {
    fn into_io_error(self) -> std::io::Error { ... }
}

impl From<ExitStatusError> for std::io::Error {...}

Improved ergonomics

This allows the programmer to use ?; the panoply of methods on Result; and crates such as anyhow and thiserror.

Added functionality

There is no new functionality here - only new more ergonomic types.

The only nontrivial functionality being added here is the Display impl.

Currently, Rust does not provide a way to print a sensible message about the fate of a child process. The Display impl for ExitStatus simply prints the numeric value. (Actually, on Unix the printed value is more properly called a "wait status", since it's a value returned from wait; this is a different value to the one passed to exit, which is a true exit status.)

We probably don't want to change the Display impl for ExitStatus (especially because people might be doing something more with it, eg putting it in logfiles which they then search, etc.). But we do want to provide something more useful. The Display impl for ExitStatusError would produce, in the usual cases, a message like one of these:

In fact I was confused and this behaviour is in the Debug impl. The Display impl is more suitable and will be made more correct in #82411.

It would be nice to change the messages to something clearer, maybe:

  exited with error exit status 4
  died due to fatal signal Terminated
  died due to fatal signal Segmentation fault (core dumped)

but that is for the future.

Example use

            Command::new(adb_path)
                .arg("push")
                .arg(&exe_file)
                .arg(&self.config.adb_test_dir)
                .status()
                .unwrap_or_else(|_| panic!("failed to exec `{:?}`", adb_path))
                .exit_ok()
                .unwrap_or_else(|e| panic!("{:?} failed: {}", adb_path, e));

In a call site using anyhow:

        (|| Ok::<_,anyhow::Error>({
            Command::new(adb_path)
                .arg("push")
                .arg(&exe_file)
                .arg(&self.config.adb_test_dir)
                .status()?
                .exit_ok()?
        }))().with_context(|| adb_path.clone())?;

Alternatives

Do nothing

This is unattractive because the existing API is cumbersome. Improving the ergnomics is necessary before making ExitStatus #[must_use], or we would be asking everyone in the community to write unergonomic code now and hope to improve it later.

Return Result<(),std::io::Error>

This would have the benefit of consistency with most other functions in std::process. However, it is not suitable.

io::Error is primarily an abstraction over operating system error codes. (It can contain other errors so that implementors of important traits such as Read are not forced to lose error information.) This can be seen in the public API but it is most convenient to summarise the internal representation, which is a three-variant enum:

  • Os(i32), containing a raw OS error code. But a wait status is not an OS error code so we cannot use this.
  • Simple(ErrorKind). The ErrorKinds correspond to (subsets of) OS errors. We would hae to use Other and completely lose the child's wait status.
  • Custom(Box<(ErrorKind, Box<dyn std::error::Error>)). That would require us to invent our own Error type, since we need something to record as the "inner error".

Inventing our own error type is necessary in any case because we need something to hang the new Display functionality on. Given that, wrapping it up in an io::Error seems perverse.

Provide Into<io::Error> as well as or instead of into_io_error(), or neither

Because most of the rest of std::process generates only operating system errors, call sites are fairly likely to be in functions themselves returning io::Result.

It seems to me that in this specific case we at the very least need to provide a convenient explicit conversion facility to help smooth the recovery from the ecosystem breakage which will expect when we make ExitStatus #[must_use]. When fixing code which previously ignored the exit status, it may be awkard to bubble an entirely different error type up through a program or library. A provided conversion to io::Error would make a local fix to such code considerably easier.

While an io::Error is not the most precise representation, code that does not know better will be reasonably well served by this conversion. The risk of unexpected lossage from its use seems fairly low because anywhere that handles io::Error must already be prepared to handle "unexpected" errors.

As an alternative, we could do this as a From impl. This would be quite unusual. The Rust stdlib does not generally provide builtin conversions to generic portmanteau error types, let alone a From impl. This is particularly hazardous because of the ? autoconversion.

So I propose this:

impl ExitStatusError {
    /// The returned `io::Error` will have kind `Other` and have
    /// this `ExitStatusError` as its inner error.  This is a convenience
    /// wrapper equivalent to calling `io::Error::new`.
    /// 
    /// Provided for situations where it is not convenient to deal
    /// with a subprocess which ran but failed differently to other
    /// errors which occur when running a subprocess.
    fn into_io_error(self) -> std::io::Error { ... }
}

Provide something else, eg a method taking an error handling closure

Result is the ergonomic and idiomatic way to represent errors in Rust.

And we need something to hang the new error display functionality off. There would be nothing wrong with having that as a method on ExitStatus (although we'd have to decide what it did with success) but using the resulting interface would be clumsy in the usual case.

Provide .exit_ok on Command

This seems like a fairly obvious convenience. It would be roughly equivalent to .status()?.exit_ok(). But its error type would have to be a specific custom enum, or wrap an exit status in io::Error where it is hard to fish out again.

So this seems to have open questions. I propose to leave it for future work.

Bikesheds

What precise part of speech and grammar to use for the messages

I have suggested phrases like "exited with error exit status 4".
An alternative would be a shorter version "fatal signal Terminated",
"error exit status 4". But the latter risks confusion between the true exit status (what was passed to exit) and the wait status; this confusion is particularly bad for Rust because std::process calls the wait status an ExitStatus. Using the verb "exited" helps avoid this.

On Unix, the wait status resulting from a call to exit is the exit_status << 8. So my example "exited with error exit status 4" corresponds to std::process::exit(4) and ExitStatus(1024).

Alternative method name instead of exit_ok

The name should have the following properties:

  • It should be fairly short, since most users of Command will need it.
  • When found in a series of chained method calls, as is common with
    Command, its function should be reasonably clear.

Prior art

Most fallible functions in stdlib return a custom error type whose payload characterises the error.

In many cases these error types are thing wrappers around another type or around unit. Eg, std::path::StripPrefixError, std::char::DecodeUTF16Error. std::array::TryFromSliceError, std::num::TryFromIntError.

Some of these types wrap an original input value, or other information about the error. For example std::sync::PoisonError, std::io::IntoInnerError, std::ffi::IntoStringError, std::ffi::NulError std::str::Utf8Error, std::string::FromUtf8Error, std::ime::SystemTimeError and arguably NoneError.

Most other functions in std::process return io::Result because their failures correspond to failed operating system calls with an OS error number and corresponding ErrorKind.

ijackson added a commit to ijackson/rust that referenced this issue Feb 17, 2021
I'm pretty sure I am going want this for rust-lang#73125 and it seems like an
omission that would be in any case good to remedy.

It's a shame we don't have competent token pasting and case mangling
for use in macro_rules!.

Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
JohnTitor added a commit to JohnTitor/rust that referenced this issue Feb 22, 2021
Provide NonZero_c_* integers

I'm pretty sure I am going want this for rust-lang#73125 and it seems like an
omission that would be in any case good to remedy.

<strike>Because the raw C types are in `std`, not `core`, to achieve this we
must export the relevant macros from `core` so that `std` can use
them.  That's done with a new `num_internals` perma-unstable feature.

The macros need to take more parameters for the module to get the
types from and feature attributes to use.

I have eyeballed the docs output for core, to check that my changes to
these macros have made no difference to the core docs output.</strike>
bors added a commit to rust-lang-ci/rust that referenced this issue May 18, 2021
Provide ExitStatusError

Closes rust-lang#73125

In MR rust-lang#81452 "Add #[must_use] to [...] process::ExitStatus" we concluded that the existing arrangements in are too awkward so adding that `#[must_use]` is blocked on improving the ergonomics.

I wrote a mini-RFC-style discusion of the approach in rust-lang#73125 (comment)
@bors bors closed this as completed in e893089 May 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-feature-request Category: A feature request, i.e: not implemented / a PR. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants