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
96fn control_timeout(command: &DaemonCommand) -> Duration {
100 match command {
101 DaemonCommand::Reload
102 | DaemonCommand::ChannelSet { .. }
103 | DaemonCommand::ChannelLogout { .. } => Duration::from_secs(90),
104 _ => Duration::from_secs(20),
105 }
106}
107
108pub async fn control(path: &Path, command: DaemonCommand) -> Result<DaemonStatus, ControlError> {
110 let broken = |error: std::io::Error| ControlError::Protocol(format!("{error}"));
111 tokio::time::timeout(control_timeout(&command), async {
112 let stream = UnixStream::connect(path)
113 .await
114 .map_err(ControlError::Unavailable)?;
115 let (reader, writer) = stream.into_split();
116 let mut connection = Connection::new(
117 BufReader::new(reader),
118 writer,
119 FrameDecoder::new(MAX_CONTROL_FRAME_BYTES, Overflow::Stop),
120 );
121 for message in [
122 ClientMessage::initialize("init", "scv-control"),
123 ClientMessage::DaemonControl {
124 request_id: "control".into(),
125 command,
126 },
127 ] {
128 connection.send(&message).await.map_err(broken)?;
129 let bytes = match connection.read().await.map_err(broken)? {
130 Frame::Line(bytes) => bytes,
131 Frame::TooLarge => {
132 return Err(ControlError::Protocol(
133 "SCV status exceeds frame limit".into(),
134 ));
135 }
136 Frame::End | Frame::Truncated(_) => {
137 return Err(ControlError::Protocol(
138 "SCV daemon closed the management connection".into(),
139 ));
140 }
141 };
142 let event = serde_json::from_slice::<ServerEvent>(&bytes)
143 .map_err(|error| ControlError::Protocol(format!("{error}")))?;
144 match event {
145 ServerEvent::Initialized {
146 protocol_version: PROTOCOL_VERSION,
147 ..
148 } if matches!(message, ClientMessage::Initialize { .. }) => {}
149 ServerEvent::DaemonStatus { status, .. } => return Ok(status),
150 ServerEvent::Error { code, message, .. } => {
151 return Err(ControlError::Server { code, message });
152 }
153 _ => {
154 return Err(ControlError::Protocol(
155 "unexpected SCV management response; upgrade/restart the daemon".into(),
156 ));
157 }
158 }
159 }
160 Err(ControlError::Protocol("SCV daemon omitted status".into()))
161 })
162 .await
163 .unwrap_or(Err(ControlError::TimedOut))
164}
165
166#[cfg(test)]
167mod tests;