#![forbid(unsafe_code)]
mod connection;
pub mod fs;
mod layout;
mod secret;
pub mod text;
pub use connection::{Connection, read_frame, write_message};
pub use layout::{Layout, Stray};
pub use secret::Secret;
use anyhow::Result;
use scv_protocol::{
ClientMessage, DaemonCommand, DaemonStatus, ErrorCode, Frame, FrameDecoder, Overflow,
PROTOCOL_VERSION, ServerEvent,
};
use std::{fmt, path::Path, time::Duration};
use tokio::{io::BufReader, net::UnixStream};
const MAX_CONTROL_FRAME_BYTES: usize = 1024 * 1024;
pub const DELEGATION_DEPTH_VARIABLE: &str = "SCV_DELEGATION_DEPTH";
pub fn inherited_delegation_depth() -> Option<u32> {
parse_delegation_depth(std::env::var(DELEGATION_DEPTH_VARIABLE).ok().as_deref())
}
fn parse_delegation_depth(value: Option<&str>) -> Option<u32> {
value
.and_then(|value| value.trim().parse().ok())
.filter(|depth| *depth > 0)
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ControlError {
Unavailable(std::io::Error),
Server {
code: ErrorCode,
message: String,
},
TimedOut,
Protocol(String),
}
impl fmt::Display for ControlError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unavailable(_) => formatter
.write_str("SCV daemon unavailable; start it with `scv start` or `scv run`"),
Self::Server { message, .. } => formatter.write_str(message),
Self::TimedOut => formatter
.write_str("SCV management request timed out; query status before retrying"),
Self::Protocol(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for ControlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Unavailable(error) => Some(error),
_ => None,
}
}
}
pub async fn control(path: &Path, command: DaemonCommand) -> Result<DaemonStatus, ControlError> {
let broken = |error: std::io::Error| ControlError::Protocol(format!("{error}"));
tokio::time::timeout(Duration::from_secs(20), async {
let stream = UnixStream::connect(path)
.await
.map_err(ControlError::Unavailable)?;
let (reader, writer) = stream.into_split();
let mut connection = Connection::new(
BufReader::new(reader),
writer,
FrameDecoder::new(MAX_CONTROL_FRAME_BYTES, Overflow::Stop),
);
for message in [
ClientMessage::initialize("init", "scv-control"),
ClientMessage::DaemonControl {
request_id: "control".into(),
command,
},
] {
connection.send(&message).await.map_err(broken)?;
let bytes = match connection.read().await.map_err(broken)? {
Frame::Line(bytes) => bytes,
Frame::TooLarge => {
return Err(ControlError::Protocol(
"SCV status exceeds frame limit".into(),
));
}
Frame::End | Frame::Truncated(_) => {
return Err(ControlError::Protocol(
"SCV daemon closed the management connection".into(),
));
}
};
let event = serde_json::from_slice::<ServerEvent>(&bytes)
.map_err(|error| ControlError::Protocol(format!("{error}")))?;
match event {
ServerEvent::Initialized {
protocol_version: PROTOCOL_VERSION,
..
} if matches!(message, ClientMessage::Initialize { .. }) => {}
ServerEvent::DaemonStatus { status, .. } => return Ok(status),
ServerEvent::Error { code, message, .. } => {
return Err(ControlError::Server { code, message });
}
_ => {
return Err(ControlError::Protocol(
"unexpected SCV management response; upgrade/restart the daemon".into(),
));
}
}
}
Err(ControlError::Protocol("SCV daemon omitted status".into()))
})
.await
.unwrap_or(Err(ControlError::TimedOut))
}
#[cfg(test)]
mod tests;