Skip to main content

scv_client/
lib.rs

1//! What every local client of the SCV daemon needs, without depending on the
2//! server: the instance [`Layout`] (every path under `SCV_HOME`, and the
3//! instance's service unit name), the delegation-depth variable a delegated SCV
4//! inherits, framed reading and writing ([`Connection`], [`read_frame`]),
5//! private instance files ([`fs::replace_private`]), [`Secret`] values
6//! that never print, byte-bounded text
7//! ([`text::utf8_prefix`]), and [`control`] for daemon management requests,
8//! which fail with a typed [`ControlError`].
9
10#![forbid(unsafe_code)]
11
12mod connection;
13pub mod fs;
14mod layout;
15mod secret;
16pub mod text;
17pub use connection::{Connection, read_frame, write_message};
18pub use layout::{Layout, Stray};
19pub use secret::Secret;
20
21use anyhow::Result;
22use scv_protocol::{
23    ClientMessage, DaemonCommand, DaemonStatus, ErrorCode, Frame, FrameDecoder, Overflow,
24    PROTOCOL_VERSION, ServerEvent,
25};
26use std::{fmt, path::Path, time::Duration};
27use tokio::{io::BufReader, net::UnixStream};
28
29/// Largest management reply, counting its line ending.
30const MAX_CONTROL_FRAME_BYTES: usize = 1024 * 1024;
31
32/// Environment variable carrying a delegated process's depth; SCV sets it on
33/// every agent it starts.
34pub const DELEGATION_DEPTH_VARIABLE: &str = "SCV_DELEGATION_DEPTH";
35
36/// The delegation depth to declare in `session.start`: this process's own,
37/// when an SCV started it, so a delegated client cannot reset the count by
38/// connecting to a daemon.
39pub fn inherited_delegation_depth() -> Option<u32> {
40    parse_delegation_depth(std::env::var(DELEGATION_DEPTH_VARIABLE).ok().as_deref())
41}
42
43fn parse_delegation_depth(value: Option<&str>) -> Option<u32> {
44    value
45        .and_then(|value| value.trim().parse().ok())
46        .filter(|depth| *depth > 0)
47}
48
49/// Why a [`control`] request failed.
50#[derive(Debug)]
51#[non_exhaustive]
52pub enum ControlError {
53    /// No daemon accepted the connection: it is not running, or the socket
54    /// is stale. Nothing was sent.
55    Unavailable(std::io::Error),
56    /// The daemon refused the request. A daemon that predates a command
57    /// refuses it with [`ErrorCode::InvalidJson`], as a frame it cannot parse.
58    Server {
59        /// Why, as the daemon's stable code.
60        code: ErrorCode,
61        /// What went wrong, for people.
62        message: String,
63    },
64    /// No answer within the helper's time limit. The daemon may still carry
65    /// out a mutation; query status before retrying.
66    TimedOut,
67    /// The exchange broke off, or the daemon answered something this client
68    /// does not understand. A mutation's outcome is unknown.
69    Protocol(String),
70}
71
72impl fmt::Display for ControlError {
73    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Self::Unavailable(_) => formatter
76                .write_str("SCV daemon unavailable; start it with `scv start` or `scv run`"),
77            Self::Server { message, .. } => formatter.write_str(message),
78            Self::TimedOut => formatter
79                .write_str("SCV management request timed out; query status before retrying"),
80            Self::Protocol(message) => formatter.write_str(message),
81        }
82    }
83}
84
85impl std::error::Error for ControlError {
86    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
87        match self {
88            Self::Unavailable(error) => Some(error),
89            _ => None,
90        }
91    }
92}
93
94/// A bounded management exchange. Never retries mutations on ambiguous failure.
95pub async fn control(path: &Path, command: DaemonCommand) -> Result<DaemonStatus, ControlError> {
96    let broken = |error: std::io::Error| ControlError::Protocol(format!("{error}"));
97    tokio::time::timeout(Duration::from_secs(20), async {
98        let stream = UnixStream::connect(path)
99            .await
100            .map_err(ControlError::Unavailable)?;
101        let (reader, writer) = stream.into_split();
102        let mut connection = Connection::new(
103            BufReader::new(reader),
104            writer,
105            FrameDecoder::new(MAX_CONTROL_FRAME_BYTES, Overflow::Stop),
106        );
107        for message in [
108            ClientMessage::initialize("init", "scv-control"),
109            ClientMessage::DaemonControl {
110                request_id: "control".into(),
111                command,
112            },
113        ] {
114            connection.send(&message).await.map_err(broken)?;
115            let bytes = match connection.read().await.map_err(broken)? {
116                Frame::Line(bytes) => bytes,
117                Frame::TooLarge => {
118                    return Err(ControlError::Protocol(
119                        "SCV status exceeds frame limit".into(),
120                    ));
121                }
122                Frame::End | Frame::Truncated(_) => {
123                    return Err(ControlError::Protocol(
124                        "SCV daemon closed the management connection".into(),
125                    ));
126                }
127            };
128            let event = serde_json::from_slice::<ServerEvent>(&bytes)
129                .map_err(|error| ControlError::Protocol(format!("{error}")))?;
130            match event {
131                ServerEvent::Initialized {
132                    protocol_version: PROTOCOL_VERSION,
133                    ..
134                } if matches!(message, ClientMessage::Initialize { .. }) => {}
135                ServerEvent::DaemonStatus { status, .. } => return Ok(status),
136                ServerEvent::Error { code, message, .. } => {
137                    return Err(ControlError::Server { code, message });
138                }
139                _ => {
140                    return Err(ControlError::Protocol(
141                        "unexpected SCV management response; upgrade/restart the daemon".into(),
142                    ));
143                }
144            }
145        }
146        Err(ControlError::Protocol("SCV daemon omitted status".into()))
147    })
148    .await
149    .unwrap_or(Err(ControlError::TimedOut))
150}
151
152#[cfg(test)]
153mod tests;