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}