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 {
match crate::ipc::daemon_control_roundtrip(
&endpoint,
crate::ipc::DaemonControlRequest::Shutdown,
None,
)
.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) if is_daemon_unreachable_err(&e) => Ok(false),
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 {
match crate::ipc::daemon_control_roundtrip(
&endpoint,
crate::ipc::DaemonControlRequest::Status,
None,
)
.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) if is_daemon_unreachable_err(&e) => {
Err(format!("daemon not running at {endpoint}: {e}"))
}
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}"))?;
let wire = crate::protocol::wire_prost::full_family_wire_format_from_env();
let request = crate::protocol::Request::SessionStart {
client_pid: std::process::id(),
working_dir: cwd.into(),
log_file,
track_stats,
journal_path,
profile: false,
private_daemon: None,
};
conn.send_request(&request, wire)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv_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);
}
};
let wire = crate::protocol::wire_prost::full_family_wire_format_from_env();
let request = crate::protocol::Request::SessionEnd {
session_id: session_id.clone(),
};
conn.send_request(&request, wire).await?;
match conn.recv_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}"))?;
let wire = crate::protocol::wire_prost::full_family_wire_format_from_env();
let request = crate::protocol::Request::SessionStats {
session_id: session_id.clone(),
};
conn.send_request(&request, wire)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv_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}"))?;
let wire = crate::protocol::wire_prost::full_family_wire_format_from_env();
let request = crate::protocol::Request::FingerprintCheck {
cache_file: cache_file.into(),
cache_type,
root: root.into(),
extensions,
include_globs,
exclude,
};
conn.send_request(&request, wire)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv_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(),
}
};
let wire = crate::protocol::wire_prost::full_family_wire_format_from_env();
conn.send_request(&request, wire)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv_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}"))?;
let wire = crate::protocol::wire_prost::full_family_wire_format_from_env();
let request = crate::protocol::Request::FingerprintInvalidate {
cache_file: cache_file.into(),
};
conn.send_request(&request, wire)
.await
.map_err(|e| format!("failed to send to daemon: {e}"))?;
match conn.recv_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}")),
}
})
}