use crate::core::NormalizedPath;
use std::path::Path;
use super::{connect_client, ensure_daemon, resolve_endpoint, run_async};
#[derive(Debug, Clone)]
pub struct SessionStartResponse {
pub session_id: String,
pub journal_path: Option<String>,
}
pub fn client_start(endpoint: Option<&str>) -> Result<(), String> {
let endpoint = resolve_endpoint(endpoint);
run_async(async move { ensure_daemon(&endpoint).await })
}
pub fn client_stop(endpoint: Option<&str>) -> Result<bool, String> {
let endpoint = resolve_endpoint(endpoint);
run_async(async move {
let mut conn = match connect_client(&endpoint).await {
Ok(c) => c,
Err(_) => return Ok(false),
};
conn.send(&crate::protocol::Request::Shutdown)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv::<crate::protocol::Response>().await {
Ok(Some(crate::protocol::Response::ShuttingDown)) => Ok(true),
Ok(Some(crate::protocol::Response::Error { message })) => Err(message),
Ok(None) => Err("lost connection to daemon (no response received)".to_string()),
Ok(Some(other)) => Err(format!("unexpected response from daemon: {other:?}")),
Err(e) => Err(format!("broken connection to daemon: {e}")),
}
})
}
pub fn client_status(endpoint: Option<&str>) -> Result<crate::protocol::DaemonStatus, String> {
let endpoint = resolve_endpoint(endpoint);
run_async(async move {
let mut conn = connect_client(&endpoint)
.await
.map_err(|e| format!("daemon not running at {endpoint}: {e}"))?;
conn.send(&crate::protocol::Request::Status)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv::<crate::protocol::Response>().await {
Ok(Some(crate::protocol::Response::Status(status))) => Ok(status),
Ok(Some(crate::protocol::Response::Error { message })) => Err(message),
Ok(None) => Err("lost connection to daemon (no response received)".to_string()),
Ok(Some(other)) => Err(format!("unexpected response from daemon: {other:?}")),
Err(e) => Err(format!("broken connection to daemon: {e}")),
}
})
}
pub fn client_session_start(
endpoint: Option<&str>,
cwd: &Path,
log_file: Option<&Path>,
track_stats: bool,
journal_path: Option<&Path>,
) -> Result<SessionStartResponse, String> {
let endpoint = resolve_endpoint(endpoint);
let cwd = cwd.to_path_buf();
let log_file = log_file.map(NormalizedPath::from);
let journal_path = journal_path.map(NormalizedPath::from);
run_async(async move {
ensure_daemon(&endpoint).await?;
let mut conn = connect_client(&endpoint)
.await
.map_err(|e| format!("cannot connect to daemon at {endpoint}: {e}"))?;
conn.send(&crate::protocol::Request::SessionStart {
client_pid: std::process::id(),
working_dir: cwd.into(),
log_file,
track_stats,
journal_path,
profile: false,
private_daemon: None,
})
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv::<crate::protocol::Response>().await {
Ok(Some(crate::protocol::Response::SessionStarted {
session_id,
journal_path,
})) => Ok(SessionStartResponse {
session_id,
journal_path: journal_path.map(|p| p.display().to_string()),
}),
Ok(Some(crate::protocol::Response::Error { message })) => Err(message),
Ok(None) => Err("lost connection to daemon (no response received)".to_string()),
Ok(Some(other)) => Err(format!("unexpected response from daemon: {other:?}")),
Err(e) => Err(format!("broken connection to daemon: {e}")),
}
})
}
pub fn client_session_end(
endpoint: Option<&str>,
session_id: &str,
) -> Result<Option<crate::protocol::SessionStats>, String> {
let endpoint = resolve_endpoint(endpoint);
session_end_idempotent(&endpoint, session_id).map_err(|e| e.to_string())
}
#[must_use]
pub fn is_daemon_unreachable_err(err: &crate::ipc::IpcError) -> bool {
use std::io::ErrorKind;
match err {
crate::ipc::IpcError::Io(io) => matches!(
io.kind(),
ErrorKind::NotFound | ErrorKind::ConnectionRefused | ErrorKind::BrokenPipe
),
_ => false,
}
}
pub fn session_end_idempotent(
endpoint: &str,
session_id: &str,
) -> Result<Option<crate::protocol::SessionStats>, crate::ipc::IpcError> {
let endpoint = endpoint.to_string();
let session_id = session_id.to_string();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| {
crate::ipc::IpcError::Endpoint(format!("failed to create tokio runtime: {e}"))
})?;
runtime.block_on(async move {
let mut conn = match connect_client(&endpoint).await {
Ok(c) => c,
Err(e) => {
if is_daemon_unreachable_err(&e) {
eprintln!(
"session-end: daemon unreachable at {endpoint}, treating session {session_id} as ended"
);
return Ok(None);
}
return Err(e);
}
};
conn.send(&crate::protocol::Request::SessionEnd {
session_id: session_id.clone(),
})
.await?;
match conn.recv::<crate::protocol::Response>().await? {
Some(crate::protocol::Response::SessionEnded { stats }) => Ok(stats),
Some(crate::protocol::Response::Error { message }) => Err(
crate::ipc::IpcError::Endpoint(format!("session-end failed: {message}")),
),
None => Err(crate::ipc::IpcError::ConnectionClosed),
Some(other) => Err(crate::ipc::IpcError::Endpoint(format!(
"unexpected response from daemon: {other:?}"
))),
}
})
}
pub fn client_session_stats(
endpoint: Option<&str>,
session_id: &str,
) -> Result<Option<crate::protocol::SessionStats>, String> {
let endpoint = resolve_endpoint(endpoint);
let session_id = session_id.to_string();
run_async(async move {
let mut conn = connect_client(&endpoint)
.await
.map_err(|e| format!("cannot connect to daemon at {endpoint}: {e}"))?;
conn.send(&crate::protocol::Request::SessionStats {
session_id: session_id.clone(),
})
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv::<crate::protocol::Response>().await {
Ok(Some(crate::protocol::Response::SessionStatsResult { stats })) => Ok(stats),
Ok(Some(crate::protocol::Response::Error { message })) => Err(message),
Ok(None) => Err("lost connection to daemon (no response received)".to_string()),
Ok(Some(other)) => Err(format!("unexpected response from daemon: {other:?}")),
Err(e) => Err(format!("broken connection to daemon: {e}")),
}
})
}
#[derive(Debug, Clone)]
pub struct FingerprintCheckResponse {
pub decision: String,
pub reason: Option<String>,
pub changed_files: Vec<String>,
}
pub fn fingerprint_check(
endpoint: Option<&str>,
cache_file: &Path,
cache_type: &str,
root: &Path,
extensions: &[String],
include_globs: &[String],
exclude: &[String],
) -> Result<FingerprintCheckResponse, String> {
let endpoint = resolve_endpoint(endpoint);
let cache_file = cache_file.to_path_buf();
let cache_type = cache_type.to_string();
let root = root.to_path_buf();
let extensions = extensions.to_vec();
let include_globs = include_globs.to_vec();
let exclude = exclude.to_vec();
run_async(async move {
ensure_daemon(&endpoint).await?;
let mut conn = connect_client(&endpoint)
.await
.map_err(|e| format!("cannot connect to daemon at {endpoint}: {e}"))?;
conn.send(&crate::protocol::Request::FingerprintCheck {
cache_file: cache_file.into(),
cache_type,
root: root.into(),
extensions,
include_globs,
exclude,
})
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv::<crate::protocol::Response>().await {
Ok(Some(crate::protocol::Response::FingerprintCheckResult {
decision,
reason,
changed_files,
})) => Ok(FingerprintCheckResponse {
decision,
reason,
changed_files,
}),
Ok(Some(crate::protocol::Response::Error { message })) => Err(message),
Ok(None) => Err("lost connection to daemon (no response received)".to_string()),
Ok(Some(other)) => Err(format!("unexpected response from daemon: {other:?}")),
Err(e) => Err(format!("broken connection to daemon: {e}")),
}
})
}
pub fn fingerprint_mark_success(endpoint: Option<&str>, cache_file: &Path) -> Result<(), String> {
fingerprint_mark(endpoint, cache_file, true)
}
pub fn fingerprint_mark_failure(endpoint: Option<&str>, cache_file: &Path) -> Result<(), String> {
fingerprint_mark(endpoint, cache_file, false)
}
fn fingerprint_mark(
endpoint: Option<&str>,
cache_file: &Path,
success: bool,
) -> Result<(), String> {
let endpoint = resolve_endpoint(endpoint);
let cache_file = cache_file.to_path_buf();
run_async(async move {
ensure_daemon(&endpoint).await?;
let mut conn = connect_client(&endpoint)
.await
.map_err(|e| format!("cannot connect to daemon at {endpoint}: {e}"))?;
let request = if success {
crate::protocol::Request::FingerprintMarkSuccess {
cache_file: cache_file.into(),
}
} else {
crate::protocol::Request::FingerprintMarkFailure {
cache_file: cache_file.into(),
}
};
conn.send(&request)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv::<crate::protocol::Response>().await {
Ok(Some(crate::protocol::Response::FingerprintAck)) => Ok(()),
Ok(Some(crate::protocol::Response::Error { message })) => Err(message),
Ok(None) => Err("lost connection to daemon (no response received)".to_string()),
Ok(Some(other)) => Err(format!("unexpected response from daemon: {other:?}")),
Err(e) => Err(format!("broken connection to daemon: {e}")),
}
})
}
pub fn fingerprint_invalidate(endpoint: Option<&str>, cache_file: &Path) -> Result<(), String> {
let endpoint = resolve_endpoint(endpoint);
let cache_file = cache_file.to_path_buf();
run_async(async move {
ensure_daemon(&endpoint).await?;
let mut conn = connect_client(&endpoint)
.await
.map_err(|e| format!("cannot connect to daemon at {endpoint}: {e}"))?;
conn.send(&crate::protocol::Request::FingerprintInvalidate {
cache_file: cache_file.into(),
})
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv::<crate::protocol::Response>().await {
Ok(Some(crate::protocol::Response::FingerprintAck)) => Ok(()),
Ok(Some(crate::protocol::Response::Error { message })) => Err(message),
Ok(None) => Err("lost connection to daemon (no response received)".to_string()),
Ok(Some(other)) => Err(format!("unexpected response from daemon: {other:?}")),
Err(e) => Err(format!("broken connection to daemon: {e}")),
}
})
}