use std::collections::VecDeque;
use std::io::{ErrorKind, Read, Write};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use serde_json::value::RawValue;
use serde_json::Value;
use crate::debug::{describe_endpoint, error_label, join_capabilities, on_off, Category, DebugLog};
use crate::diffing::build_delta;
use crate::error::Error;
use crate::framing::{encode_frame, FrameDecoder};
use crate::limits::{Limits, DEFAULT_LIMITS};
use crate::logs::{AttrValue, LogLevel, LogRecord, MAX_LOG_ATTRS};
use crate::marker::encode_marker;
use crate::messages::{
default_capabilities, parse_driver_message, GetTree, GetTreeResult, Hello, HelloAck,
LogMessage, ProbeInfo, ProtocolErrorMessage, RevisionCommit, SnapshotMessage,
};
use crate::roles::Capability;
use crate::tree::Snapshot;
use crate::validate::validate_snapshot;
pub const ENV_ENDPOINT: &str = "TERMWRIGHT_ENDPOINT";
pub const ENV_TOKEN: &str = "TERMWRIGHT_TOKEN";
pub const ENV_PROTOCOL: &str = "TERMWRIGHT_PROTOCOL";
pub const DIAL_TIMEOUT: Duration = Duration::from_secs(5);
pub const WRITE_TIMEOUT: Duration = Duration::from_millis(250);
const SNAPSHOT_HISTORY: usize = 8;
#[derive(Debug, Clone)]
pub struct Options {
pub adapter_name: String,
pub adapter_version: String,
pub capabilities: Vec<Capability>,
pub limits: Limits,
pub write_timeout: Option<Duration>,
pub probe: Option<ProbeInfo>,
pub debug: Option<Arc<DebugLog>>,
}
impl Options {
pub fn with_logs(adapter_name: impl Into<String>, adapter_version: impl Into<String>) -> Self {
let mut options = Self::new(adapter_name, adapter_version);
options.capabilities.push(Capability::Logs);
options
}
pub fn new(adapter_name: impl Into<String>, adapter_version: impl Into<String>) -> Self {
Self {
adapter_name: adapter_name.into(),
adapter_version: adapter_version.into(),
capabilities: default_capabilities(),
limits: DEFAULT_LIMITS,
write_timeout: Some(WRITE_TIMEOUT),
probe: None,
debug: None,
}
}
}
fn epoch_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|since| since.as_millis() as i64)
.unwrap_or(0)
}
#[derive(Debug)]
struct TokenBucket {
per_second: f64,
capacity: f64,
tokens: f64,
updated: Instant,
}
impl TokenBucket {
fn new(per_second: i64, burst: i64, now: Instant) -> Self {
let rate = per_second.max(0) as f64;
let capacity = rate + burst.max(0) as f64;
Self {
per_second: rate,
capacity,
tokens: capacity,
updated: now,
}
}
fn take(&mut self, now: Instant) -> bool {
if self.per_second <= 0.0 {
return false;
}
let elapsed = now.saturating_duration_since(self.updated).as_secs_f64();
self.updated = now;
self.tokens = (self.tokens + elapsed * self.per_second).min(self.capacity);
if self.tokens < 1.0 {
return false;
}
self.tokens -= 1.0;
true
}
}
#[derive(Debug)]
pub struct Client {
endpoint: String,
token: String,
options: Options,
protocol: String,
stream: Option<UnixStream>,
decoder: FrameDecoder,
limits: Limits,
session_id: Option<String>,
revision: i64,
marker_enabled: bool,
log_budget: Option<crate::messages::LogBudget>,
published: Option<Value>,
deltas_sent: u64,
snapshots_sent: u64,
log_seq: i64,
log_bucket: Option<TokenBucket>,
logs_dropped: u64,
subscribe: String,
history: VecDeque<(i64, Box<RawValue>)>,
force_full: bool,
}
impl Client {
pub fn new(endpoint: impl Into<String>, token: impl Into<String>, options: Options) -> Self {
let limits = options.limits;
Self {
endpoint: endpoint.into(),
token: token.into(),
options,
protocol: crate::messages::PROTOCOL_ID.into(),
stream: None,
decoder: FrameDecoder::new(limits.max_frame_bytes, limits.max_depth),
limits,
session_id: None,
revision: 0,
marker_enabled: false,
log_budget: None,
published: None,
deltas_sent: 0,
snapshots_sent: 0,
log_seq: 0,
log_bucket: None,
logs_dropped: 0,
subscribe: "snapshots".to_owned(),
history: VecDeque::new(),
force_full: false,
}
}
#[must_use]
pub fn qualified_observations(&self) -> bool {
self.protocol == crate::messages::PROTOCOL_V2_ID
}
pub fn from_env(mut options: Options) -> Option<Self> {
if options.debug.is_none() {
options.debug = DebugLog::from_env(&options.adapter_name).map(Arc::new);
}
Self::from_values(
std::env::var(ENV_ENDPOINT).ok().as_deref(),
std::env::var(ENV_TOKEN).ok().as_deref(),
std::env::var(ENV_PROTOCOL).ok().as_deref(),
options,
)
}
pub fn from_values(
endpoint: Option<&str>,
token: Option<&str>,
protocol: Option<&str>,
options: Options,
) -> Option<Self> {
let endpoint = endpoint.filter(|value| !value.is_empty());
let token = token.filter(|value| !value.is_empty());
let (Some(endpoint), Some(token)) = (endpoint, token) else {
if let Some(log) = options.debug.as_ref() {
let mut missing = Vec::new();
if endpoint.is_none() {
missing.push(ENV_ENDPOINT);
}
if token.is_none() {
missing.push(ENV_TOKEN);
}
log.line(
Category::Diag,
&format!("dormant: {} not set", missing.join(" and ")),
);
}
return None;
};
if let Some(protocol) = protocol.filter(|value| !value.is_empty()) {
if protocol != crate::messages::PROTOCOL_ID
&& protocol != crate::messages::PROTOCOL_V2_ID
&& protocol != "1"
&& protocol != "2"
{
if let Some(log) = options.debug.as_ref() {
log.line(
Category::Diag,
&format!(
"dormant: {ENV_PROTOCOL}={protocol:?} is not {:?}",
crate::messages::PROTOCOL_ID
),
);
}
return None;
}
}
if endpoint.starts_with(r"\\.\pipe\") || endpoint.starts_with(r"\\?\pipe\") {
if let Some(log) = options.debug.as_ref() {
log.line(
Category::Diag,
&format!(
"dormant: {} needs a Windows transport this client does not have",
describe_endpoint(endpoint)
),
);
}
return None;
}
let v2 = matches!(protocol, Some(crate::messages::PROTOCOL_V2_ID) | Some("2"));
let mut client = Self::new(endpoint, token, options);
if v2 {
client.protocol = crate::messages::PROTOCOL_V2_ID.into();
if !client
.options
.capabilities
.contains(&Capability::QualifiedObservations)
{
client
.options
.capabilities
.push(Capability::QualifiedObservations);
}
}
Some(client)
}
pub fn connect(&mut self, timeout: Duration) -> Result<(), Error> {
self.debug_line(
Category::Sem,
&format!(
"dial {} timeout={}ms",
describe_endpoint(&self.endpoint),
timeout.as_millis()
),
);
let stream = match UnixStream::connect(&self.endpoint) {
Ok(stream) => stream,
Err(error) => {
self.debug_line(
Category::Diag,
&format!("dial failed, staying dormant: {}", error_label(&error)),
);
return Err(error.into());
}
};
stream.set_read_timeout(Some(Duration::from_millis(50)))?;
stream.set_write_timeout(self.options.write_timeout)?;
self.stream = Some(stream);
let mut hello = Hello::new(
&self.token,
&self.options.adapter_name,
&self.options.adapter_version,
self.options.capabilities.clone(),
);
hello.protocol = self.protocol.clone();
if let Some(probe) = self.options.probe.clone() {
hello = hello.with_probe(probe);
}
self.send(&hello)?;
self.debug_line(
Category::Sem,
&format!(
"hello sent adapter={}/{} caps={}",
self.options.adapter_name,
self.options.adapter_version,
join_capabilities(&self.options.capabilities)
),
);
let deadline = Instant::now() + timeout;
while self.session_id.is_none() {
if Instant::now() >= deadline {
self.debug_line(
Category::Diag,
&format!(
"no hello-ack within {}ms, staying dormant",
timeout.as_millis()
),
);
self.close();
return Err(Error::HandshakeTimeout);
}
self.poll()?;
}
Ok(())
}
fn debug_line(&self, category: Category, message: &str) {
if let Some(log) = self.options.debug.as_ref() {
log.line(category, message);
}
}
pub fn connected(&self) -> bool {
self.session_id.is_some() && self.stream.is_some()
}
pub fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
pub fn revision(&self) -> i64 {
self.revision
}
pub fn log_budget(&self) -> Option<crate::messages::LogBudget> {
self.log_budget
}
pub fn limits(&self) -> &Limits {
&self.limits
}
pub fn require_full_snapshot(&mut self) {
self.force_full = true;
}
#[must_use]
pub fn full_snapshot_required(&self) -> bool {
self.force_full
}
pub fn close(&mut self) {
if let Some(stream) = self.stream.take() {
self.debug_line(
Category::Sem,
&format!(
"close r{} snapshots={} deltas={} logs_dropped={}",
self.revision, self.snapshots_sent, self.deltas_sent, self.logs_dropped
),
);
let _ = stream.shutdown(std::net::Shutdown::Both);
}
self.session_id = None;
}
pub fn publish(&mut self, snapshot: &mut Snapshot) -> Result<Option<String>, Error> {
let result = self.publish_inner(snapshot);
if matches!(result, Err(Error::Protocol(_) | Error::Validation(_))) {
self.require_full_snapshot();
}
result
}
fn publish_inner(&mut self, snapshot: &mut Snapshot) -> Result<Option<String>, Error> {
let Some(session_id) = self.session_id.clone() else {
return Ok(None);
};
if self.stream.is_none() {
return Ok(None);
}
let revision = self.revision + 1;
snapshot.v = if self.protocol == crate::messages::PROTOCOL_V2_ID {
2
} else {
1
};
snapshot.session_id = session_id.clone();
snapshot.revision = revision;
let body = serde_json::to_string(&snapshot).map_err(|_| {
Error::Protocol(crate::error::Violation::new(
"frame-malformed",
"snapshot is not JSON-serialisable",
))
})?;
let parsed: Value = serde_json::from_str(&body).expect("just serialised");
validate_snapshot(&parsed, &self.limits)?;
let marker = if self.marker_enabled {
Some(encode_marker(&self.token, &session_id, revision)?)
} else {
None
};
let mut sent_delta = false;
let tree_frame = if self.subscribe != "revisions" {
let forced = self.force_full;
if forced {
self.debug_line(
Category::Io,
&format!("r{revision} full snapshot: the producer reported a gap"),
);
}
let delta = if self.subscribe == "diffs" && !forced {
self.published
.as_ref()
.and_then(|base| build_delta(base, &parsed))
} else {
None
};
match delta {
Some(delta) => {
sent_delta = true;
Some(encode_frame(&delta, self.limits.max_frame_bytes)?)
}
None => Some(encode_frame(
&SnapshotMessage::new(snapshot),
self.limits.max_frame_bytes,
)?),
}
} else {
None
};
let commit_frame =
encode_frame(&RevisionCommit::new(revision), self.limits.max_frame_bytes)?;
if let Some(frame) = &tree_frame {
self.write_frame(frame)?;
}
self.write_frame(&commit_frame)?;
self.revision = revision;
self.remember(revision, RawValue::from_string(body).expect("valid JSON"));
if tree_frame.is_some() {
self.published = Some(parsed);
self.force_full = false;
if sent_delta {
self.deltas_sent += 1;
} else {
self.snapshots_sent += 1;
}
}
Ok(marker)
}
pub fn deltas_sent(&self) -> u64 {
self.deltas_sent
}
pub fn snapshots_sent(&self) -> u64 {
self.snapshots_sent
}
pub fn logs_dropped(&self) -> u64 {
self.logs_dropped
}
pub fn log(&mut self, mut record: LogRecord) -> bool {
if self.session_id.is_none() || self.stream.is_none() || self.log_bucket.is_none() {
return false;
}
let origin = record.seq;
self.log_seq += 1;
record.seq = self.log_seq;
if record.ts == 0 {
record.ts = epoch_millis();
}
if record.revision.is_none() && self.revision > 0 {
record.revision = Some(self.revision);
}
let now = Instant::now();
let allowed = self
.log_bucket
.as_mut()
.is_some_and(|bucket| bucket.take(now));
if !allowed {
self.logs_dropped += 1;
return false;
}
if origin > 0 && record.attrs.len() < MAX_LOG_ATTRS {
record
.attrs
.insert("origin.seq".to_owned(), AttrValue::Int(origin));
if record.validate(&self.limits).is_err() {
record.attrs.remove("origin.seq");
}
}
if record.validate(&self.limits).is_err() {
self.logs_dropped += 1;
return false;
}
self.send(&LogMessage::new(&record)).is_ok()
}
pub fn log_message(&mut self, level: LogLevel, message: impl Into<String>) -> bool {
self.log(LogRecord::new(level, message))
}
pub fn poll(&mut self) -> Result<(), Error> {
let mut buffer = [0u8; 8192];
loop {
let read = match self.stream.as_mut() {
None => return Ok(()),
Some(stream) => stream.read(&mut buffer),
};
match read {
Ok(0) => {
self.close();
return Ok(());
}
Ok(count) => {
let frames = self.decoder.push(&buffer[..count])?;
for frame in frames {
self.handle(&frame.value)?;
}
}
Err(error)
if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) =>
{
return Ok(())
}
Err(error) if error.kind() == ErrorKind::Interrupted => continue,
Err(error) => {
self.close();
return Err(Error::Io(error));
}
}
}
}
fn handle(&mut self, value: &Value) -> Result<(), Error> {
if let Err(error) = parse_driver_message(value, &self.limits) {
self.debug_line(
Category::Diag,
&format!("rejected a driver message: {error}"),
);
let _ = self.send(&ProtocolErrorMessage::new("malformed", error.to_string()));
self.close();
return Err(Error::Parse(error));
}
match value.get("type").and_then(Value::as_str) {
Some("hello-ack") => {
let ack: HelloAck = serde_json::from_value(value.clone()).expect("validated above");
self.session_id = Some(ack.session_id);
self.limits = ack.limits;
self.marker_enabled = ack.marker.enabled;
self.log_budget = ack.logs;
self.log_bucket = match ack.logs {
Some(budget) if budget.enabled => Some(TokenBucket::new(
budget.max_records_per_second,
budget.burst,
Instant::now(),
)),
_ => None,
};
self.subscribe = ack.subscribe;
if let Some(log) = self.options.debug.as_ref() {
let session = self.session_id.clone().unwrap_or_default();
log.set_label(&session);
log.line(
Category::Sem,
&format!(
"hello-ack session={session} marker={} subscribe={} logs={}",
on_off(self.marker_enabled),
self.subscribe,
on_off(self.log_bucket.is_some())
),
);
}
}
Some("get-tree") => {
let request: GetTree =
serde_json::from_value(value.clone()).expect("validated above");
let wanted = request.revision.unwrap_or(self.revision);
let held = self
.history
.iter()
.find(|(revision, _)| *revision == wanted)
.map(|(_, body)| body.clone());
let answer = match held {
Some(body) => GetTreeResult::found(request.request_id, body),
None => GetTreeResult::missing(
request.request_id,
format!("revision {wanted} is not retained"),
),
};
self.send(&answer)?;
}
Some("error") => {
self.debug_line(
Category::Diag,
&format!(
"driver ended the session: {}",
value.get("code").and_then(Value::as_str).unwrap_or("?")
),
);
self.close();
}
_ => {}
}
Ok(())
}
fn remember(&mut self, revision: i64, body: Box<RawValue>) {
self.history.push_back((revision, body));
while self.history.len() > SNAPSHOT_HISTORY {
self.history.pop_front();
}
}
fn send<T: serde::Serialize>(&mut self, message: &T) -> Result<(), Error> {
let frame = encode_frame(message, self.limits.max_frame_bytes)?;
self.write_frame(&frame)
}
fn write_frame(&mut self, frame: &[u8]) -> Result<(), Error> {
let Some(stream) = self.stream.as_mut() else {
return Ok(());
};
match stream.write_all(frame).and_then(|()| stream.flush()) {
Ok(()) => Ok(()),
Err(error) => {
let timed_out = matches!(
error.kind(),
ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted
);
self.close();
if timed_out {
self.debug_line(
Category::Diag,
"write deadline exceeded; session is unrecoverable",
);
return Err(Error::WriteTimeout);
}
Err(Error::Io(error))
}
}
}
}