#[cfg(feature = "client")]
use std::io;
#[cfg(feature = "client")]
use std::time::{Duration, Instant};
#[cfg(feature = "client")]
use crate::broker::backend_lifecycle::identity::IdentityError;
use crate::broker::backend_lifecycle::probe;
#[cfg(feature = "client")]
use crate::broker::backend_lifecycle::probe::ProbeError;
use crate::broker::backend_lifecycle::verify_pid::ProcessHandle;
#[cfg(feature = "client")]
use crate::broker::backend_lifecycle::verify_pid::{self, VerifyPidError};
#[cfg(feature = "client")]
use crate::broker::protocol::CacheManifest;
use crate::broker::protocol::Endpoint;
pub use crate::broker::backend_lifecycle::DaemonProcess;
#[cfg(feature = "client")]
pub type Result<T> = std::result::Result<T, BackendHandleError>;
pub struct BackendHandle {
pub service_name: String,
pub service_version: String,
pub daemon_process: DaemonProcess,
pub(crate) process_handle: Option<ProcessHandle>,
}
impl BackendHandle {
pub fn probe(endpoint: &Endpoint, expected: &DaemonProcess) -> Option<Self> {
let process_handle = probe::probe_endpoint(endpoint, expected).ok()?;
Some(Self::from_verified(
String::new(),
String::new(),
expected.clone(),
process_handle,
))
}
#[cfg(feature = "client-async")]
pub async fn probe_async(endpoint: &Endpoint, expected: &DaemonProcess) -> Option<Self> {
Self::probe_with_service_async("", "", endpoint, expected)
.await
.ok()
}
#[cfg(feature = "client")]
pub fn probe_with_service(
service_name: impl Into<String>,
service_version: impl Into<String>,
endpoint: &Endpoint,
expected: &DaemonProcess,
) -> Result<Self> {
Self::probe_with_service_and_timeout(
service_name,
service_version,
endpoint,
expected,
probe::DEFAULT_ENDPOINT_PROBE_TIMEOUT,
)
}
#[cfg(feature = "client")]
pub fn probe_with_service_and_timeout(
service_name: impl Into<String>,
service_version: impl Into<String>,
endpoint: &Endpoint,
expected: &DaemonProcess,
timeout: std::time::Duration,
) -> Result<Self> {
let process_handle = probe::probe_endpoint_with_timeout(endpoint, expected, timeout)?;
Ok(Self::from_verified(
service_name.into(),
service_version.into(),
expected.clone(),
process_handle,
))
}
#[cfg(feature = "client-async")]
pub async fn probe_with_service_async(
service_name: impl Into<String>,
service_version: impl Into<String>,
endpoint: &Endpoint,
expected: &DaemonProcess,
) -> Result<Self> {
let process_handle =
crate::broker::backend_lifecycle::probe_async::probe_endpoint_async(endpoint, expected)
.await?;
Ok(Self::from_verified(
service_name.into(),
service_version.into(),
expected.clone(),
process_handle,
))
}
#[cfg(feature = "client")]
pub fn probe_manifest(manifest: &CacheManifest) -> Option<Self> {
Self::try_from_manifest(manifest).ok().flatten()
}
#[cfg(feature = "client")]
pub fn try_from_manifest(manifest: &CacheManifest) -> Result<Option<Self>> {
let Some(daemon_process) = DaemonProcess::from_manifest_current_daemon(manifest)? else {
return Ok(None);
};
let handle = Self::probe_with_service(
manifest.service_name.clone(),
manifest.service_version.clone(),
&daemon_process.ipc_endpoint,
&daemon_process,
)?;
Ok(Some(handle))
}
#[cfg(feature = "client")]
pub fn is_alive(&self) -> bool {
self.platform_handle()
.map(|handle| handle.is_alive())
.unwrap_or_else(|| verify_pid::process_is_alive(self.daemon_process.pid))
}
#[cfg(feature = "client")]
pub async fn connect(&self) -> Result<Connection> {
Connection::connect(&self.daemon_process.ipc_endpoint).map_err(BackendHandleError::Connect)
}
#[cfg(feature = "client")]
pub fn try_duplicate_windows_handoff_handle(
&self,
pipe_handle: crate::broker::server::handoff::WindowsHandleValue,
handoff_token: crate::broker::server::handoff::HandoffToken,
) -> crate::broker::server::handoff::DuplicateHandleResult {
let attempt = crate::broker::server::handoff::DuplicateHandleAttempt::new(
pipe_handle,
self.daemon_process.pid,
handoff_token,
);
crate::broker::server::handoff::try_duplicate_handle(&attempt)
}
#[cfg(feature = "client")]
pub async fn shutdown(self, timeout: Duration) -> Result<()> {
verify_pid::signal_terminate(self.daemon_process.pid)?;
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if !self.is_alive() {
remove_endpoint_socket(&self.daemon_process.ipc_endpoint);
return Ok(());
}
std::thread::sleep(Duration::from_millis(20));
}
Err(BackendHandleError::ShutdownTimeout {
pid: self.daemon_process.pid,
})
}
#[cfg(feature = "client")]
pub fn force_kill(self) -> Result<()> {
verify_pid::force_kill_pid(self.daemon_process.pid)?;
Ok(())
}
fn from_verified(
service_name: String,
service_version: String,
daemon_process: DaemonProcess,
process_handle: ProcessHandle,
) -> Self {
Self {
service_name,
service_version,
daemon_process,
process_handle: Some(process_handle),
}
}
#[cfg(feature = "client")]
fn platform_handle(&self) -> Option<&ProcessHandle> {
self.process_handle.as_ref()
}
}
#[cfg(feature = "client")]
pub struct Connection {
stream: crate::platform::ipc::Stream,
}
#[cfg(feature = "client")]
impl Connection {
pub fn connect(endpoint: &Endpoint) -> io::Result<Self> {
if endpoint.path.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"backend endpoint path is empty",
));
}
let endpoint = crate::platform::ipc::Endpoint::new(endpoint.path.clone())?;
let stream = crate::platform::ipc::Stream::connect(&endpoint)?;
Ok(Self { stream })
}
pub fn into_inner(self) -> crate::platform::ipc::Stream {
self.stream
}
}
#[cfg(feature = "client")]
#[derive(Debug, thiserror::Error)]
pub enum BackendHandleError {
#[error(transparent)]
Identity(#[from] IdentityError),
#[error(transparent)]
Probe(#[from] ProbeError),
#[error("backend IPC connection failed: {0}")]
Connect(io::Error),
#[error(transparent)]
VerifyPid(#[from] VerifyPidError),
#[error("backend shutdown timed out for pid {pid}")]
ShutdownTimeout {
pid: u32,
},
}
#[cfg(feature = "client")]
fn remove_endpoint_socket(endpoint: &Endpoint) {
if crate::platform::ipc::endpoint_is_filesystem_backed() {
let _ = std::fs::remove_file(&endpoint.path);
}
}
#[cfg(all(test, feature = "client"))]
mod endpoint_socket_tests {
use super::*;
fn endpoints_are_files() -> bool {
crate::platform::ipc::endpoint_is_filesystem_backed()
}
fn endpoint_at(path: &std::path::Path) -> Endpoint {
Endpoint {
namespace_id: "shared".into(),
path: path.display().to_string(),
}
}
#[test]
fn the_socket_file_is_removed() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("endpoint.sock");
std::fs::write(&path, b"").expect("create the stand-in socket");
assert!(path.exists(), "precondition: the file exists");
remove_endpoint_socket(&endpoint_at(&path));
if endpoints_are_files() {
assert!(!path.exists(), "the endpoint name outlived its daemon");
} else {
assert!(
path.exists(),
"a host whose endpoints are not files must not unlink one",
);
}
}
#[test]
fn an_already_removed_socket_is_not_an_error() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("never-existed.sock");
assert!(!path.exists(), "precondition: nothing to remove");
remove_endpoint_socket(&endpoint_at(&path));
}
#[test]
fn a_directory_at_the_endpoint_path_is_left_alone() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("surprise-directory");
std::fs::create_dir(&path).expect("create the directory");
remove_endpoint_socket(&endpoint_at(&path));
assert!(path.is_dir(), "cleanup removed a directory");
}
}