pueue_lib/network_blocking/
client.rs

1//! A reference implementation of a simple client that you may use.
2//! En/disable via the `client` feature.
3use color_eyre::{
4    Result,
5    eyre::{Context, bail},
6};
7use serde::Serialize;
8
9use super::protocol::*;
10use crate::{Error, PROTOCOL_VERSION, internal_prelude::*, message::*};
11
12/// This struct contains the base logic for the client.
13/// The client is responsible for connecting to the daemon, sending instructions
14/// and interpreting their responses.
15///
16/// ```no_run
17/// use std::path::PathBuf;
18/// use pueue_lib::{
19///     BlockingClient,
20///     Response,
21///     Request,
22///     network_blocking::socket::ConnectionSettings,
23/// };
24/// # use color_eyre::{Result, eyre::Context};
25///
26/// # #[cfg(target_os = "windows")]
27/// # fn main() {}
28///
29/// # #[cfg(not(target_os = "windows"))]
30/// # fn main() -> Result<()> {
31///
32/// // Connection settings and secret to connect to the daemon.
33/// let settings = ConnectionSettings::UnixSocket {
34///     path: PathBuf::from("/home/user/.local/share/pueue/pueue.socket"),
35/// };
36/// let secret = "My secret";
37///
38/// // Create a client. This already establishes a connection to the daemon.
39/// let mut client = BlockingClient::new(settings, secret.as_bytes(), true)
40///     .context("Failed to initialize client.")?;
41///
42/// // Request the state.
43/// client.send_request(Request::Status)?;
44/// let response: Response = client.receive_response()?;
45///
46/// let _state = match response {
47///     Response::Status(state) => state,
48///     _ => unreachable!(),
49/// };
50/// # Ok(())
51/// # }
52/// ```
53pub struct BlockingClient {
54    pub stream: GenericBlockingStream,
55    pub daemon_version: String,
56}
57
58impl std::fmt::Debug for BlockingClient {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("Client")
61            .field("stream", &"GenericStream<not_debuggable>")
62            .finish()
63    }
64}
65
66impl BlockingClient {
67    /// Initialize a new client.
68    /// This includes establishing a connection to the daemon:
69    ///     - Connect to the daemon.
70    ///     - Authorize via secret.
71    ///     - Check versions incompatibilities.
72    ///
73    /// If the `show_version_warning` flag is `true` and the daemon has a different version than
74    /// the client, a warning will be logged.
75    pub fn new(
76        settings: ConnectionSettings<'_>,
77        secret: &[u8],
78        show_version_warning: bool,
79    ) -> Result<Self> {
80        // Connect to daemon and get stream used for communication.
81        let mut stream = get_client_stream(settings).context("Failed to initialize stream.")?;
82
83        // Next we do a handshake with the daemon
84        // 1. Client sends the secret to the daemon.
85        // 2. If successful, the daemon responds with their version.
86        send_bytes(secret, &mut stream).context("Failed to send secret.")?;
87
88        // Receive and parse the response. We expect the daemon's version as UTF-8.
89        let version_bytes = receive_bytes(&mut stream)
90            .context("Failed to receive version during handshake with daemon.")?;
91        if version_bytes.is_empty() {
92            bail!("Daemon went away after sending secret. Did you use the correct secret?")
93        }
94        let daemon_version = match String::from_utf8(version_bytes) {
95            Ok(version) => version,
96            Err(_) => {
97                bail!("Daemon sent invalid UTF-8. Did you use the correct secret?")
98            }
99        };
100
101        // Info if the daemon runs a different protocol version.
102        // Backward compatibility should work, but some features might not work as expected.
103        if daemon_version != PROTOCOL_VERSION && show_version_warning {
104            warn!(
105                "Different protocol version detected '{daemon_version}'. Consider updating and restarting the daemon."
106            );
107        }
108
109        Ok(BlockingClient {
110            stream,
111            daemon_version,
112        })
113    }
114
115    /// Convenience function to get a mutable handle on the client's stream.
116    pub fn stream(&mut self) -> &mut GenericBlockingStream {
117        &mut self.stream
118    }
119
120    /// Convenience wrapper around [`super::send_request`] to directly send [`Request`]s.
121    pub fn send_request<T>(&mut self, message: T) -> Result<(), Error>
122    where
123        T: Into<Request>,
124        T: Serialize + std::fmt::Debug,
125    {
126        send_message::<_, Request>(message, &mut self.stream)
127    }
128
129    /// Convenience wrapper that wraps `receive_message` for [`Response`]s
130    pub fn receive_response(&mut self) -> Result<Response, Error> {
131        receive_message::<Response>(&mut self.stream)
132    }
133
134    pub fn daemon_version(&self) -> &String {
135        &self.daemon_version
136    }
137}