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 = 8 * 1024 * 1024;
35
36pub const DELEGATION_DEPTH_VARIABLE: &str = "SCV_DELEGATION_DEPTH";
39
40pub fn inherited_delegation_depth() -> Option<u32> {
44 parse_delegation_depth(std::env::var(DELEGATION_DEPTH_VARIABLE).ok().as_deref())
45}
46
47fn parse_delegation_depth(value: Option<&str>) -> Option<u32> {
48 value
49 .and_then(|value| value.trim().parse().ok())
50 .filter(|depth| *depth > 0)
51}
52
53#[derive(Debug)]
55#[non_exhaustive]
56pub enum ControlError {
57 Unavailable(std::io::Error),
60 Server {
63 code: ErrorCode,
65 message: String,
67 },
68 TimedOut,
71 Protocol(String),
74}
75
76impl fmt::Display for ControlError {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::Unavailable(_) => formatter
80 .write_str("SCV daemon unavailable; start it with `scv start` or `scv run`"),
81 Self::Server { message, .. } => formatter.write_str(message),
82 Self::TimedOut => formatter
83 .write_str("SCV management request timed out; query status before retrying"),
84 Self::Protocol(message) => formatter.write_str(message),
85 }
86 }
87}
88
89impl std::error::Error for ControlError {
90 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
91 match self {
92 Self::Unavailable(error) => Some(error),
93 _ => None,
94 }
95 }
96}
97
98pub fn control_timeout(command: &DaemonCommand) -> Duration {
107 match command {
108 DaemonCommand::Reload
109 | DaemonCommand::ChannelSet { .. }
110 | DaemonCommand::ChannelLogout { .. }
111 | DaemonCommand::WorkbenchStart { .. }
112 | DaemonCommand::WorkbenchResume { .. }
113 | DaemonCommand::WorkbenchSend { .. }
114 | DaemonCommand::Worktrees
115 | DaemonCommand::WorktreeRemove { .. }
116 | DaemonCommand::Attach { .. } => Duration::from_secs(90),
117 _ => Duration::from_secs(20),
118 }
119}
120
121pub async fn control(path: &Path, command: DaemonCommand) -> Result<DaemonStatus, ControlError> {
123 let broken = |error: std::io::Error| ControlError::Protocol(format!("{error}"));
124 tokio::time::timeout(control_timeout(&command), async {
125 let stream = UnixStream::connect(path)
126 .await
127 .map_err(ControlError::Unavailable)?;
128 let (reader, writer) = stream.into_split();
129 let mut connection = Connection::new(
130 BufReader::new(reader),
131 writer,
132 FrameDecoder::new(MAX_CONTROL_FRAME_BYTES, Overflow::Stop),
133 );
134 for message in [
135 ClientMessage::initialize("init", "scv-control"),
136 ClientMessage::DaemonControl {
137 request_id: "control".into(),
138 command,
139 },
140 ] {
141 connection.send(&message).await.map_err(broken)?;
142 let bytes = match connection.read().await.map_err(broken)? {
143 Frame::Line(bytes) => bytes,
144 Frame::TooLarge => {
145 return Err(ControlError::Protocol(
146 "SCV status exceeds frame limit".into(),
147 ));
148 }
149 Frame::End | Frame::Truncated(_) => {
150 return Err(ControlError::Protocol(
151 "SCV daemon closed the management connection".into(),
152 ));
153 }
154 };
155 let event = serde_json::from_slice::<ServerEvent>(&bytes)
156 .map_err(|error| ControlError::Protocol(format!("{error}")))?;
157 match event {
158 ServerEvent::Initialized {
159 protocol_version: PROTOCOL_VERSION,
160 ..
161 } if matches!(message, ClientMessage::Initialize { .. }) => {}
162 ServerEvent::DaemonStatus { status, .. } => return Ok(status),
163 ServerEvent::Error { code, message, .. } => {
164 return Err(ControlError::Server { code, message });
165 }
166 _ => {
167 return Err(ControlError::Protocol(
168 "unexpected SCV management response; upgrade/restart the daemon".into(),
169 ));
170 }
171 }
172 }
173 Err(ControlError::Protocol("SCV daemon omitted status".into()))
174 })
175 .await
176 .unwrap_or(Err(ControlError::TimedOut))
177}
178
179#[cfg(test)]
180mod tests;