1#![forbid(unsafe_code)]
12
13mod connection;
14pub mod fs;
15pub mod history;
16mod layout;
17mod secret;
18pub mod text;
19pub use connection::{Connection, read_frame, write_message};
20pub use layout::{Layout, Stray};
21pub use secret::Secret;
22
23use anyhow::Result;
24use scv_protocol::{
25 ClientMessage, DaemonCommand, DaemonStatus, ErrorCode, Frame, FrameDecoder, Overflow,
26 PROTOCOL_VERSION, ServerEvent,
27};
28use std::{fmt, path::Path, time::Duration};
29use tokio::{io::BufReader, net::UnixStream};
30
31const MAX_CONTROL_FRAME_BYTES: usize = 1024 * 1024;
33
34pub const DELEGATION_DEPTH_VARIABLE: &str = "SCV_DELEGATION_DEPTH";
37
38pub fn inherited_delegation_depth() -> Option<u32> {
42 parse_delegation_depth(std::env::var(DELEGATION_DEPTH_VARIABLE).ok().as_deref())
43}
44
45fn parse_delegation_depth(value: Option<&str>) -> Option<u32> {
46 value
47 .and_then(|value| value.trim().parse().ok())
48 .filter(|depth| *depth > 0)
49}
50
51#[derive(Debug)]
53#[non_exhaustive]
54pub enum ControlError {
55 Unavailable(std::io::Error),
58 Server {
61 code: ErrorCode,
63 message: String,
65 },
66 TimedOut,
69 Protocol(String),
72}
73
74impl fmt::Display for ControlError {
75 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 Self::Unavailable(_) => formatter
78 .write_str("SCV daemon unavailable; start it with `scv start` or `scv run`"),
79 Self::Server { message, .. } => formatter.write_str(message),
80 Self::TimedOut => formatter
81 .write_str("SCV management request timed out; query status before retrying"),
82 Self::Protocol(message) => formatter.write_str(message),
83 }
84 }
85}
86
87impl std::error::Error for ControlError {
88 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
89 match self {
90 Self::Unavailable(error) => Some(error),
91 _ => None,
92 }
93 }
94}
95
96pub async fn control(path: &Path, command: DaemonCommand) -> Result<DaemonStatus, ControlError> {
98 let broken = |error: std::io::Error| ControlError::Protocol(format!("{error}"));
99 tokio::time::timeout(Duration::from_secs(20), async {
100 let stream = UnixStream::connect(path)
101 .await
102 .map_err(ControlError::Unavailable)?;
103 let (reader, writer) = stream.into_split();
104 let mut connection = Connection::new(
105 BufReader::new(reader),
106 writer,
107 FrameDecoder::new(MAX_CONTROL_FRAME_BYTES, Overflow::Stop),
108 );
109 for message in [
110 ClientMessage::initialize("init", "scv-control"),
111 ClientMessage::DaemonControl {
112 request_id: "control".into(),
113 command,
114 },
115 ] {
116 connection.send(&message).await.map_err(broken)?;
117 let bytes = match connection.read().await.map_err(broken)? {
118 Frame::Line(bytes) => bytes,
119 Frame::TooLarge => {
120 return Err(ControlError::Protocol(
121 "SCV status exceeds frame limit".into(),
122 ));
123 }
124 Frame::End | Frame::Truncated(_) => {
125 return Err(ControlError::Protocol(
126 "SCV daemon closed the management connection".into(),
127 ));
128 }
129 };
130 let event = serde_json::from_slice::<ServerEvent>(&bytes)
131 .map_err(|error| ControlError::Protocol(format!("{error}")))?;
132 match event {
133 ServerEvent::Initialized {
134 protocol_version: PROTOCOL_VERSION,
135 ..
136 } if matches!(message, ClientMessage::Initialize { .. }) => {}
137 ServerEvent::DaemonStatus { status, .. } => return Ok(status),
138 ServerEvent::Error { code, message, .. } => {
139 return Err(ControlError::Server { code, message });
140 }
141 _ => {
142 return Err(ControlError::Protocol(
143 "unexpected SCV management response; upgrade/restart the daemon".into(),
144 ));
145 }
146 }
147 }
148 Err(ControlError::Protocol("SCV daemon omitted status".into()))
149 })
150 .await
151 .unwrap_or(Err(ControlError::TimedOut))
152}
153
154#[cfg(test)]
155mod tests;