1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use crate::docker::remote;
use crate::temp;

use std::sync::atomic::{AtomicBool, Ordering};

pub use color_eyre::Section;
pub use eyre::Context;
pub use eyre::Result;

pub static mut TERMINATED: AtomicBool = AtomicBool::new(false);

pub fn install_panic_hook() -> Result<()> {
    color_eyre::config::HookBuilder::new()
        .display_env_section(false)
        .install()
}

/// # Safety
/// Safe as long as we have single-threaded execution.
unsafe fn termination_handler() {
    // we can't warn the user here, since locks aren't signal-safe.
    // we can delete files, since fdopendir is thread-safe, and
    // `openat`, `unlinkat`, and `lstat` are signal-safe.
    //  https://man7.org/linux/man-pages/man7/signal-safety.7.html
    if !TERMINATED.swap(true, Ordering::SeqCst) && temp::has_tempfiles() {
        temp::clean();
    }

    // tl;dr this is a long explanation to say this is thread-safe.
    // due to the risk for UB with this code, it's important we get
    // this right.
    //
    // this code is wrapped in a sequentially-consistent ordering
    // for strong guarantees about signal safety. since all atomics
    // are guaranteed to be lock free, and must ensure consistent swaps
    // among all threads, this means that we should only ever have one
    // entry into the drop, which makes the external commands safe.
    //
    // to quote the reference on signal safety for linux:
    // > In general, a function is async-signal-safe either because it
    // > is reentrant or because it is atomic with respect to signals
    // > (i.e., its execution can't be interrupted by a signal handler).
    //
    // https://man7.org/linux/man-pages/man7/signal-safety.7.html
    //
    // our operations are atomic, and this is the generally-recommended
    // approach for signal-safe functions that modify global state (note
    // that this is C, but the same rules apply except volatile vars):
    // https://wiki.sei.cmu.edu/confluence/display/c/SIG31-C.+Do+not+access+shared+objects+in+signal+handlers
    //
    // even if the execution of the child process was interrupted by a
    // signal handler, it's an external process that doesn't modify the
    // environment variables of the parent, nor is it likely to lock
    // (except on windows). therefore, for most cases, this code inside
    // the atomic lock guard will still be lock-free, and therefore async
    // signal-safe. in general, the implementations for spawning a child
    // process in rust have the following logic:
    // 1. get a rw-lock for the environment variables, and read them.
    // 2. exec the child process
    //
    // the rw-lock allows any number of readers, which since we're not
    // writing any environment variables, should be async-signal safe:
    // it won't deadlock. it could technically lock if we had something
    // writing environment variables to a child process, and the execution
    // was multi-threaded, but we simply don't do that.
    //
    // for spawning the child process, on unix, the spawn is done via
    // `posix_spawnp`, which is async-signal safe on linux, although
    // it is not guaranteed to be async-signal safe on POSIX in general:
    //      https://bugzilla.kernel.org/show_bug.cgi?id=25292
    //
    // even for POSIX, it's basically thread-safe for our invocations,
    // since we do not modify the environment for our invocations:
    // >  It is also complicated to modify the environment of a multi-
    // > threaded process temporarily, since all threads must agree
    // > when it is safe for the environment to be changed. However,
    // > this cost is only borne by those invocations of posix_spawn()
    // > and posix_spawnp() that use the additional functionality.
    // > Since extensive modifications are not the usual case,
    // > and are particularly unlikely in time-critical code,
    // > keeping much of the environment control out of posix_spawn()
    // > and posix_spawnp() is appropriate design.
    //
    // https://pubs.opengroup.org/onlinepubs/009695399/functions/posix_spawn.html
    //
    // on windows, a non-reentrant static mutex is used, so it is
    // definitely not thread safe, but this should not matter.
    remote::CONTAINER = None;

    // EOWNERDEAD, seems to be the same on linux, macos, and bash on windows.
    std::process::exit(130);
}

pub fn install_termination_hook() -> Result<()> {
    // SAFETY: safe since single-threaded execution.
    ctrlc::set_handler(|| unsafe { termination_handler() }).map_err(Into::into)
}

#[derive(Debug, thiserror::Error)]
pub enum CommandError {
    #[error("`{command}` failed with {status}")]
    NonZeroExitCode {
        status: std::process::ExitStatus,
        command: String,
        stderr: Vec<u8>,
        stdout: Vec<u8>,
    },
    #[error("could not execute `{command}`")]
    CouldNotExecute {
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
        command: String,
    },
    #[error("`{0:?}` output was not UTF-8")]
    Utf8Error(#[source] std::string::FromUtf8Error, std::process::Output),
}

impl CommandError {
    /// Attach valuable information to this [`CommandError`](Self)
    pub fn to_section_report(self) -> eyre::Report {
        match &self {
            CommandError::NonZeroExitCode { stderr, stdout, .. } => {
                let stderr = String::from_utf8_lossy(stderr).trim().to_owned();
                let stdout = String::from_utf8_lossy(stdout).trim().to_owned();
                eyre::eyre!(self)
                    .section(color_eyre::SectionExt::header(stderr, "Stderr:"))
                    .section(color_eyre::SectionExt::header(stdout, "Stdout:"))
            }
            _ => eyre::eyre!(self),
        }
    }
}