shep 0.18.2

The shep binary: a process manager that keeps a flock of long-running processes alive on macOS, Linux and Windows, with logs, watch and cron restarts, and webhook alerts
Documentation
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! `shep dial-stdio`: the remote end of the ssh transport.
//!
//! `ssh <dest> shep dial-stdio` runs this on the machine that holds the
//! shepherd, and the existing length-prefixed frames cross unchanged. The verb
//! copies bytes and nothing else: it does no handshake, no version check, and
//! never looks inside a frame.
//!
//! With `--autostart` it first boots the shepherd when none answers, which is
//! how `shep --host <dest> muster` brings up a host that has just rebooted.
//! No other verb asks for it.

use std::convert::Infallible;
use std::io;
use std::path::Path;
use std::process::Child;

use shep_client::spawn::{SpawnError, connect_or_spawn};
use shep_client::{ConnectError, HANDSHAKE_TIMEOUT};
use shep_core::paths::ShepPaths;
use shep_core::transport;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};

use crate::cli::Format;
use crate::client::emit_error_locked;
use crate::exit::ExitCode;
use crate::launch::launch_daemon;

/// Copies bytes between stdin/stdout and the shepherd at `paths.socket`, then
/// ends the process. With `autostart`, a shepherd is booted first if none
/// answers.
///
/// Never returns. tokio's stdin read runs on a blocking thread that dropping
/// the runtime waits for, so returning an [`ExitCode`] would hang the process
/// on an open stdin after the shepherd side closed. Stdout is flushed by
/// [`bridge`] before this exits.
pub(crate) async fn dial_stdio(fmt: Format, paths: &ShepPaths, autostart: bool) -> Infallible {
    let launch_paths = paths.clone();
    let code = dial(
        fmt,
        &paths.socket,
        autostart,
        move || launch_daemon(&launch_paths),
        tokio::io::stdin(),
        tokio::io::stdout(),
    )
    .await;
    std::process::exit(i32::from(code as u8));
}

/// Connects to the shepherd at `socket` and runs [`bridge`] over it.
///
/// With `autostart`, `launch` boots the shepherd when none answers, through
/// the state machine a local `muster` uses. Its handshake is spent: the client
/// it returns is dropped and the bridge dials afresh, so the bridge stays the
/// same bytes-only copy with or without the flag. A shepherd that refuses the
/// probe on a protocol mismatch is already up, so that case bridges too.
/// Without `autostart`, `launch` is never called.
///
/// Nothing is written to `output` when no shepherd answers or the boot fails:
/// the one error goes to stderr.
async fn dial<I, O, L>(
    fmt: Format,
    socket: &Path,
    autostart: bool,
    launch: L,
    input: I,
    output: O,
) -> ExitCode
where
    I: AsyncRead + Unpin,
    O: AsyncWrite + Unpin,
    L: FnOnce() -> io::Result<Child> + Send + 'static,
{
    if autostart {
        match connect_or_spawn(socket, launch).await {
            Ok(_spent) => {}
            // A shepherd that refuses on version skew is still answering. The
            // bridge copies bytes, so the operator's client judges the skew
            // and names it, as it would without the flag.
            Err(SpawnError::Connect(ConnectError::ProtocolMismatch { .. })) => {}
            Err(err) => {
                let code = ExitCode::from(&err);
                emit_error_locked(fmt, code, &err.to_string());
                return code;
            }
        }
    }
    let unreachable = |why: &dyn core::fmt::Display| {
        emit_error_locked(
            fmt,
            ExitCode::DaemonUnreachable,
            &format!("no shepherd answered at {}: {why}", socket.display()),
        );
        ExitCode::DaemonUnreachable
    };
    let stream = match tokio::time::timeout(HANDSHAKE_TIMEOUT, transport::connect(socket)).await {
        Ok(Ok(stream)) => stream,
        Ok(Err(err)) => return unreachable(&err),
        Err(_) => return unreachable(&"timed out"),
    };
    match bridge(input, output, stream).await {
        Ok(()) => ExitCode::Success,
        Err(err) => {
            emit_error_locked(fmt, ExitCode::Failure, &format!("connection lost: {err}"));
            ExitCode::Failure
        }
    }
}

/// Copies `input` to `shepherd` and `shepherd` to `output` until either side
/// reaches end of file or errors, then flushes `output`.
///
/// End of `input` shuts down the write half toward the shepherd, so the
/// shepherd sees an orderly end rather than a reset. A Windows named pipe has
/// no half-close: its shutdown only flushes, and the shepherd sees the end when
/// the pipe handle closes, which for [`dial_stdio`] is the process exiting.
///
/// # Errors
///
/// The first I/O error on either direction, or on the final flush.
async fn bridge<I, O, S>(mut input: I, mut output: O, shepherd: S) -> io::Result<()>
where
    I: AsyncRead + Unpin,
    O: AsyncWrite + Unpin,
    S: AsyncRead + AsyncWrite,
{
    let (mut from_shepherd, mut to_shepherd) = tokio::io::split(shepherd);
    let ended = {
        let upstream = async {
            tokio::io::copy(&mut input, &mut to_shepherd).await?;
            to_shepherd.shutdown().await
        };
        let downstream = async {
            tokio::io::copy(&mut from_shepherd, &mut output)
                .await
                .map(drop)
        };
        tokio::select! {
            ended = upstream => ended,
            ended = downstream => ended,
        }
    };
    ended.and(output.flush().await)
}

#[cfg(test)]
mod tests {
    use core::time::Duration;

    use tokio::io::{AsyncReadExt, duplex};

    use super::*;

    /// Long enough for a loaded CI machine, short enough to fail as a named
    /// assertion rather than a hung run.
    const BOUND: Duration = Duration::from_secs(10);

    const BLOB_BYTES: usize = 4 * 1024 * 1024;

    /// Every byte value once, then a blob whose bytes come from a small LCG so
    /// a shifted or dropped run changes the comparison.
    fn payloads() -> [Vec<u8>; 2] {
        let every_value = (0..=u8::MAX).collect();
        let mut state = 0x9e37_79b9_u32;
        let blob = (0..BLOB_BYTES)
            .map(|_| {
                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
                (state >> 24) as u8
            })
            .collect();
        [every_value, blob]
    }

    #[tokio::test]
    async fn bytes_cross_both_ways_unchanged() {
        for payload in payloads() {
            // stdin to the shepherd: the bridge ends on end of input. The
            // bridge owns `client`, so the shepherd's end of file arrives on
            // every platform when it returns.
            let (mut server, client) = transport::connected_pair().await.unwrap();
            let (mut stdin_tx, stdin) = duplex(64 * 1024);
            let (stdout, _stdout_rx) = duplex(64 * 1024);
            let (bridged, (), received) = tokio::time::timeout(BOUND, async {
                tokio::join!(
                    bridge(stdin, stdout, client),
                    async {
                        stdin_tx.write_all(&payload).await.unwrap();
                        drop(stdin_tx);
                    },
                    async {
                        let mut received = Vec::new();
                        server.read_to_end(&mut received).await.unwrap();
                        received
                    },
                )
            })
            .await
            .expect("upstream copy finished");
            bridged.unwrap();
            assert!(received == payload, "stdin to shepherd altered the bytes");

            // The shepherd to stdout: the bridge ends when the shepherd closes.
            let (mut server, client) = transport::connected_pair().await.unwrap();
            let (_stdin_tx, stdin) = duplex(64 * 1024);
            let (stdout, mut stdout_rx) = duplex(64 * 1024);
            let (bridged, (), received) = tokio::time::timeout(BOUND, async {
                tokio::join!(
                    bridge(stdin, stdout, client),
                    async {
                        server.write_all(&payload).await.unwrap();
                        drop(server);
                    },
                    async {
                        let mut received = Vec::new();
                        stdout_rx.read_to_end(&mut received).await.unwrap();
                        received
                    },
                )
            })
            .await
            .expect("downstream copy finished");
            bridged.unwrap();
            assert!(received == payload, "shepherd to stdout altered the bytes");
        }
    }

    #[tokio::test]
    async fn the_bridge_ends_when_the_shepherd_side_closes_with_input_still_open() {
        let (server, client) = transport::connected_pair().await.unwrap();
        let (_stdin_tx, stdin) = duplex(1024);
        let (stdout, _stdout_rx) = duplex(1024);
        drop(server);

        let bridged = tokio::time::timeout(BOUND, bridge(stdin, stdout, client))
            .await
            .expect("the bridge outlived the shepherd side");
        bridged.unwrap();
    }

    #[tokio::test]
    async fn end_of_input_closes_the_write_half_toward_the_shepherd_and_ends_the_bridge() {
        let (mut server, mut client) = transport::connected_pair().await.unwrap();
        let (mut stdin_tx, stdin) = duplex(1024);
        let (stdout, _stdout_rx) = duplex(1024);
        stdin_tx.write_all(b"last words").await.unwrap();
        drop(stdin_tx);

        // `client` is lent, so it is still open when the bridge returns: on
        // unix the shepherd's end of file below comes from the shutdown, not a
        // drop.
        let bridged = tokio::time::timeout(BOUND, bridge(stdin, stdout, &mut client))
            .await
            .expect("the bridge outlived end of input");
        bridged.unwrap();
        // A named pipe has no half-close, so only closing the handle ends the
        // shepherd's read there.
        #[cfg(windows)]
        drop(client);

        let mut received = Vec::new();
        tokio::time::timeout(BOUND, server.read_to_end(&mut received))
            .await
            .expect("the shepherd never saw end of file")
            .unwrap();
        assert_eq!(received, b"last words");
    }

    #[tokio::test]
    async fn a_missing_socket_is_daemon_unreachable_and_writes_nothing_to_the_output_half() {
        let dir = tempfile::tempdir().unwrap();
        let socket = dir.path().join("run").join("shep.sock");
        let (_stdin_tx, stdin) = duplex(1024);
        let (stdout, mut stdout_rx) = duplex(1024);

        let code = dial(Format::Table, &socket, false, no_launch, stdin, stdout).await;

        assert_eq!(code, ExitCode::DaemonUnreachable);
        let mut written = Vec::new();
        stdout_rx.read_to_end(&mut written).await.unwrap();
        assert!(written.is_empty(), "stdout got {written:?}");
    }

    /// A launcher for a run that must not boot anything.
    fn no_launch() -> io::Result<Child> {
        unreachable!("no flag asked for an autostart")
    }

    #[tokio::test]
    async fn without_autostart_nothing_is_launched() {
        let dir = tempfile::tempdir().unwrap();
        let socket = shep_client::testing::control_address(dir.path());
        let (_stdin_tx, stdin) = duplex(1024);
        let (stdout, _stdout_rx) = duplex(1024);

        // The launcher panics, and a panic inside it resumes on the caller.
        let code = dial(Format::Table, &socket, false, no_launch, stdin, stdout).await;

        assert_eq!(code, ExitCode::DaemonUnreachable);
    }

    /// Unix only: the fake shepherd accepts the autostart handshake alone, and
    /// a named pipe has no second connection to bridge over.
    #[cfg(unix)]
    #[tokio::test]
    async fn autostart_boots_a_shepherd_before_bridging() {
        use core::sync::atomic::{AtomicBool, Ordering};
        use std::sync::Arc;

        let dir = tempfile::tempdir().unwrap();
        let socket = shep_client::testing::control_address(dir.path());
        let launched = Arc::new(AtomicBool::new(false));
        let (stdin_tx, stdin) = duplex(1024);
        let (stdout, mut stdout_rx) = duplex(1024);
        drop(stdin_tx);
        let launcher = {
            let (socket, launched) = (socket.clone(), Arc::clone(&launched));
            move || {
                launched.store(true, Ordering::SeqCst);
                shep_client::testing::start_fake_daemon_answering_on(&socket);
                // The losing side of a cold-start race: a child that is not
                // the daemon, which the state machine tolerates.
                shep_client::testing::child_exiting_with(shep_client::spawn::DAEMON_ALREADY_RUNNING)
            }
        };

        let code = tokio::time::timeout(
            BOUND,
            dial(Format::Table, &socket, true, launcher, stdin, stdout),
        )
        .await
        .expect("the bridge outlived end of input");

        assert!(launched.load(Ordering::SeqCst), "nothing was launched");
        assert_eq!(code, ExitCode::Success);
        let mut written = Vec::new();
        stdout_rx.read_to_end(&mut written).await.unwrap();
        assert!(written.is_empty(), "stdout got {written:?}");
    }

    /// A shepherd outside this binary's protocol range is still a shepherd:
    /// the probe's refusal is not the bridge's to report, and the operator's
    /// client names the skew itself. Unix only, like the autostart test: a
    /// named pipe has no second connection to bridge over.
    #[cfg(unix)]
    #[tokio::test]
    async fn a_protocol_mismatch_on_the_probe_still_bridges() {
        use shep_core::protocol::{HelloReply, RpcError, RpcErrorCode, encode_frame};
        use tokio::io::AsyncReadExt as _;

        let dir = tempfile::tempdir().unwrap();
        let socket = shep_client::testing::control_address(dir.path());
        let mut listener = transport::Listener::bind(&socket).unwrap();
        let refusal: HelloReply = Err(RpcError {
            code: RpcErrorCode::ProtocolMismatch,
            message: "daemon speaks protocol 2, client speaks 1".into(),
            daemon_version: None,
        });
        let refusal = encode_frame(&refusal).unwrap();
        let shepherd = tokio::spawn(async move {
            // The probe: read its hello frame, refuse it, close.
            let mut probe = listener.accept().await.unwrap();
            let mut prefix = [0_u8; 4];
            probe.read_exact(&mut prefix).await.unwrap();
            let mut hello = vec![0_u8; u32::from_be_bytes(prefix) as usize];
            probe.read_exact(&mut hello).await.unwrap();
            probe
                .write_all(&u32::try_from(refusal.len()).unwrap().to_be_bytes())
                .await
                .unwrap();
            probe.write_all(&refusal).await.unwrap();
            drop(probe);
            // The bridge: bytes the dialler never has to understand.
            let mut bridged = listener.accept().await.unwrap();
            bridged.write_all(b"frames").await.unwrap();
        });
        let (_stdin_tx, stdin) = duplex(1024);
        let (stdout, mut stdout_rx) = duplex(1024);

        let code = tokio::time::timeout(
            BOUND,
            dial(Format::Table, &socket, true, no_launch, stdin, stdout),
        )
        .await
        .expect("the bridge outlived the shepherd side");

        assert_eq!(code, ExitCode::Success);
        let mut written = Vec::new();
        stdout_rx.read_to_end(&mut written).await.unwrap();
        assert_eq!(written, b"frames");
        shepherd.await.unwrap();
    }

    #[tokio::test]
    async fn a_failed_autostart_exits_with_its_spawn_code_and_writes_nothing_to_stdout() {
        let dir = tempfile::tempdir().unwrap();
        let socket = shep_client::testing::control_address(dir.path());
        let (_stdin_tx, stdin) = duplex(1024);
        let (stdout, mut stdout_rx) = duplex(1024);
        let refused = || Err(io::Error::other("no such binary"));

        let code = dial(Format::Table, &socket, true, refused, stdin, stdout).await;

        assert_eq!(code, ExitCode::DaemonUnreachable);
        let mut written = Vec::new();
        stdout_rx.read_to_end(&mut written).await.unwrap();
        assert!(written.is_empty(), "stdout got {written:?}");
    }
}