Skip to main content

scv_client/
lib.rs

1//! Shared local transport interfaces, without server policy or bridge dependencies.
2
3use anyhow::{Context, Result, bail};
4use scv_protocol::{
5    ClientMessage, DaemonCommand, DaemonStatus, PROTOCOL_VERSION, PeerInfo, ServerEvent,
6};
7use std::{
8    path::{Path, PathBuf},
9    time::Duration,
10};
11use tokio::{
12    io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
13    net::UnixStream,
14};
15
16/// Environment variable carrying a delegated process's depth; SCV sets it on
17/// every agent it starts.
18pub const DELEGATION_DEPTH_VARIABLE: &str = "SCV_DELEGATION_DEPTH";
19
20/// The delegation depth to declare in `session.start`: this process's own,
21/// when an SCV started it, so a delegated client cannot reset the count by
22/// connecting to a daemon.
23pub fn inherited_delegation_depth() -> Option<u32> {
24    parse_delegation_depth(std::env::var(DELEGATION_DEPTH_VARIABLE).ok().as_deref())
25}
26
27fn parse_delegation_depth(value: Option<&str>) -> Option<u32> {
28    value
29        .and_then(|value| value.trim().parse().ok())
30        .filter(|depth| *depth > 0)
31}
32
33pub fn default_socket_path() -> Result<PathBuf> {
34    let root = std::env::var_os("SCV_HOME")
35        .map(PathBuf::from)
36        .or_else(|| dirs::home_dir().map(|path| path.join(".scv")))
37        .context("cannot determine SCV_HOME")?;
38    Ok(root.join("server.sock"))
39}
40
41/// A bounded management exchange. Never retries mutations on ambiguous failure.
42pub async fn control(path: &Path, command: DaemonCommand) -> Result<DaemonStatus> {
43    tokio::time::timeout(Duration::from_secs(20), async {
44        let stream = UnixStream::connect(path)
45            .await
46            .context("SCV daemon unavailable; start it with `scv start` or `scv run`")?;
47        let (reader, mut writer) = stream.into_split();
48        let mut reader = BufReader::new(reader);
49        for message in [
50            ClientMessage::Initialize {
51                request_id: "init".into(),
52                protocol_version: PROTOCOL_VERSION,
53                client: PeerInfo {
54                    name: "scv-control".into(),
55                    version: env!("CARGO_PKG_VERSION").into(),
56                },
57            },
58            ClientMessage::DaemonControl {
59                request_id: "control".into(),
60                command,
61            },
62        ] {
63            let mut frame = serde_json::to_vec(&message)?;
64            frame.push(b'\n');
65            writer.write_all(&frame).await?;
66            let mut bytes = Vec::new();
67            loop {
68                let buf = reader.fill_buf().await?;
69                if buf.is_empty() {
70                    bail!("SCV daemon closed the management connection");
71                }
72                let take = buf
73                    .iter()
74                    .position(|b| *b == b'\n')
75                    .map_or(buf.len(), |n| n + 1);
76                if bytes.len() + take > 1024 * 1024 {
77                    bail!("SCV status exceeds frame limit");
78                }
79                bytes.extend_from_slice(&buf[..take]);
80                reader.consume(take);
81                if bytes.last() == Some(&b'\n') {
82                    break;
83                }
84            }
85            match serde_json::from_slice::<ServerEvent>(&bytes)? {
86                ServerEvent::Initialized {
87                    protocol_version: PROTOCOL_VERSION,
88                    ..
89                } if matches!(message, ClientMessage::Initialize { .. }) => {}
90                ServerEvent::DaemonStatus { status, .. } => return Ok(status),
91                ServerEvent::Error { message, .. } => bail!("{message}"),
92                _ => bail!("unexpected SCV management response; upgrade/restart the daemon"),
93            }
94        }
95        bail!("SCV daemon omitted status")
96    })
97    .await
98    .context("SCV management request timed out; query status before retrying")?
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn only_a_positive_inherited_depth_is_declared() {
107        assert_eq!(parse_delegation_depth(Some("2")), Some(2));
108        assert_eq!(parse_delegation_depth(Some(" 1\n")), Some(1));
109        assert_eq!(parse_delegation_depth(Some("0")), None);
110        assert_eq!(parse_delegation_depth(Some("deep")), None);
111        assert_eq!(parse_delegation_depth(None), None);
112    }
113}