use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::Duration;
use super::protocol::{route, ApiError, Info, ResolveRequest, WorktreeInfo};
use super::machine::MachineRendezvous;
const EXCHANGE_TIMEOUT: Duration = Duration::from_secs(10);
const REGISTER_TIMEOUT: Duration = Duration::from_secs(120);
pub struct Client {
base: String,
worktree: Mutex<Option<WorktreeInfo>>,
control_token: String,
http: reqwest::blocking::Client,
}
impl std::fmt::Debug for Client {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Client")
.field("base", &self.base)
.field("worktree", &self.worktree_info().map(|w| w.id))
.finish_non_exhaustive()
}
}
impl Clone for Client {
fn clone(&self) -> Self {
Self {
base: self.base.clone(),
worktree: Mutex::new(self.worktree_info()),
control_token: self.control_token.clone(),
http: self.http.clone(),
}
}
}
impl Client {
pub fn from_rendezvous(rendezvous: &MachineRendezvous) -> Result<Self, String> {
let http = reqwest::blocking::Client::builder()
.timeout(EXCHANGE_TIMEOUT)
.connect_timeout(EXCHANGE_TIMEOUT)
.no_proxy()
.build()
.map_err(|e| format!("cannot build the daemon client: {e}"))?;
Ok(Self {
base: format!(
"http://{}",
std::net::SocketAddr::new(dialable(rendezvous.http_address), rendezvous.http_port)
),
worktree: Mutex::new(None),
control_token: rendezvous.control_token.clone(),
http,
})
}
pub fn resolve_worktree(&self, cwd: &Path) -> Result<WorktreeInfo, String> {
let body = ResolveRequest {
cwd: cwd.display().to_string(),
};
let request = self
.http
.post(self.url(route::RESOLVE))
.timeout(REGISTER_TIMEOUT)
.json(&body);
let info: WorktreeInfo = self.send(request, route::RESOLVE)?;
check_resolution(cwd, &info)?;
if let Ok(mut held) = self.worktree.lock() {
*held = Some(info.clone());
}
Ok(info)
}
pub fn worktree_info(&self) -> Option<WorktreeInfo> {
self.worktree.lock().ok().and_then(|held| held.clone())
}
fn worktree_id(&self) -> Result<String, String> {
self.worktree_info().map(|info| info.id).ok_or_else(|| {
"this client has not been told which worktree it is for; \
call resolve_worktree first"
.to_string()
})
}
pub fn worktree_root(&self) -> Option<PathBuf> {
self.worktree_info().map(|info| PathBuf::from(info.root))
}
pub fn workbench_url(&self) -> &str {
&self.base
}
pub fn info(&self) -> Result<Info, String> {
let request = self.http.get(self.url(route::INFO));
self.send(request, route::INFO)
}
pub fn shutdown(&self) -> Result<(), String> {
let mut request = self
.http
.post(self.url(route::SHUTDOWN))
.json(&serde_json::json!({}));
if !self.control_token.is_empty() {
request = request.header(route::CONTROL_HEADER, &self.control_token);
}
self.send::<serde_json::Value>(request, route::SHUTDOWN)
.map(|_| ())
}
pub fn operator_get_json<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, String> {
let request = self
.http
.get(self.url(path))
.header(route::CONTROL_HEADER, &self.control_token);
self.send(request, path)
}
pub fn get_json<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, String> {
let request = self.http.get(self.url(path));
self.send(request, path)
}
pub fn worktree_post<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &impl serde::Serialize,
) -> Result<T, String> {
let id = self.worktree_id()?;
self.post_json(&format!("/api/v1/worktrees/{id}{path}"), body)
}
pub fn worktree_get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, String> {
let id = self.worktree_id()?;
self.get_json(&format!("/api/v1/worktrees/{id}{path}"))
}
pub fn post_json<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &impl serde::Serialize,
) -> Result<T, String> {
let request = self.http.post(self.url(path)).json(body);
self.send(request, path)
}
pub fn delete_json<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, String> {
let request = self.http.delete(self.url(path));
self.send(request, path)
}
pub fn delete_json_with<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &impl serde::Serialize,
) -> Result<T, String> {
let request = self.http.delete(self.url(path)).json(body);
self.send(request, path)
}
fn url(&self, path: &str) -> String {
format!("{}{path}", self.base)
}
fn send<T: serde::de::DeserializeOwned>(
&self,
request: reqwest::blocking::RequestBuilder,
path: &str,
) -> Result<T, String> {
let response = request
.send()
.map_err(|e| format!("the daemon is not answering at {}{path}: {e}", self.base))?;
let status = response.status();
let body = response
.text()
.map_err(|e| format!("the daemon's answer to {path} could not be read: {e}"))?;
if !status.is_success() {
let message = serde_json::from_str::<ApiError>(&body)
.ok()
.map(|api| api.error)
.unwrap_or_else(|| body.trim().to_string());
return Err(if message.is_empty() {
format!("the daemon refused {path} with status {status}")
} else {
message
});
}
serde_json::from_str(&body)
.map_err(|e| format!("the daemon's answer to {path} was not one: {e}"))
}
}
pub(crate) fn check_resolution(cwd: &Path, info: &WorktreeInfo) -> Result<(), String> {
let root = PathBuf::from(&info.root);
let inside = match (crate::platform::canonicalize(cwd), crate::platform::canonicalize(&root)) {
(Ok(cwd), Ok(root)) => cwd.starts_with(&root),
_ => cwd.starts_with(&root),
};
if inside {
Ok(())
} else {
Err(format!(
"the daemon answered {} with worktree {} at {}, which does not contain it; \
restart it (`wsb daemon restart`) — it is serving one worktree's state to another",
cwd.display(),
info.id,
info.root
))
}
}
pub(crate) fn dialable(listening: std::net::IpAddr) -> std::net::IpAddr {
if listening.is_unspecified() {
match listening {
std::net::IpAddr::V4(_) => std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
std::net::IpAddr::V6(_) => std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
}
} else {
listening
}
}
#[cfg(test)]
#[path = "client_tests.rs"]
mod tests;