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