Skip to main content

scv_client/
lib.rs

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