1#![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
29const MAX_CONTROL_FRAME_BYTES: usize = 1024 * 1024;
31
32pub const DELEGATION_DEPTH_VARIABLE: &str = "SCV_DELEGATION_DEPTH";
35
36pub 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#[derive(Debug)]
51#[non_exhaustive]
52pub enum ControlError {
53 Unavailable(std::io::Error),
56 Server {
59 code: ErrorCode,
61 message: String,
63 },
64 TimedOut,
67 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
94pub 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;