chilen_ipc/lib.rs
1#![doc = include_str!("../README.md")]
2
3pub mod library;
4pub mod playback;
5
6use std::{
7 env::temp_dir,
8 io::{BufReader, Write},
9 path::PathBuf,
10};
11
12pub use interprocess::local_socket::Stream;
13use interprocess::local_socket::{GenericFilePath, GenericNamespaced, Name, ToNsName, prelude::*};
14use log::{error, info, trace};
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 library::{LibraryCommand, LibraryError, MusicLibrary},
19 playback::{PlaybackCommand, PlaybackError, PlaybackEvent, PlaybackResponse},
20};
21
22/// The default name of the socket the daemon listens on.
23///
24/// This can be used for testing, but please do not use this socket name in a finished project.
25pub const DEFAULT_SOCKET_NAME: &str = "DEFAULT_MUSIC_PLAYER.socket";
26
27/// Error related to the `daemon`.
28///
29/// Can either originate from a [`Response`] or from a function in this crate.
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub enum Error {
32 /// The provided command could not be encoded.
33 EncodingError,
34 /// The `daemon` response could not be decoded.
35 DecodingError,
36 /// Could not connect to the daemon.
37 ConnectionError,
38 /// Could not send the command to the daemon.
39 SendingError,
40 /// Error related to the music library.
41 LibraryError(LibraryError),
42 /// The response received from the daemon was unexpected or invalid.
43 InvalidResponse,
44 /// Error related to the playback module.
45 PlaybackError(PlaybackError),
46 /// Could not obtain a socket
47 SocketError(String),
48}
49
50impl std::fmt::Display for Error {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::EncodingError => write!(f, "Could not encode the daemon command"),
54 Self::DecodingError => write!(f, "Could not decode the response from the daemon"),
55 Self::ConnectionError => write!(f, "Could not connect to the daemon"),
56 Self::SendingError => write!(f, "Could not send the command to the daemon"),
57 Self::LibraryError(e) => {
58 write!(f, "{e}")
59 }
60 Self::InvalidResponse => {
61 write!(f, "The response from the daemon was invalid or malformed")
62 }
63 Self::PlaybackError(e) => write!(f, "Playback error: {e}"),
64 Self::SocketError(e) => write!(f, "Could not obtain a socket: {e}"),
65 }
66 }
67}
68
69/// Event from the `daemon` received in [`Response::Event`].
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub enum Event {
72 /// Sent before the `daemon` closes.
73 Shutdown,
74 /// Sent after the contents of the music library have changed.
75 LibraryChanged(MusicLibrary),
76 /// Sent when a client disconnects from the daemon.
77 ConnectionClosed,
78 /// Event originating from the playback module of the daemon.
79 PlaybackEvent(PlaybackEvent),
80}
81
82/// Response sent to a client from the `daemon`.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub enum Response {
85 /// The client command was executed successfully.
86 Ok,
87 /// Response to [`Command::Ping`].
88 Pong,
89 /// The contents of the music library.
90 Library(MusicLibrary),
91 /// An event from the daemon.
92 Event(Event),
93 /// Response from the playback module.
94 Playback(PlaybackResponse),
95 /// The client command has failed.
96 Error(Error),
97}
98
99/// Command that can be executed by a `daemon` instance.
100///
101/// The expected response may be different depending on the command sent. If it isn't specified in
102/// the variant documentation, assume [`Response::Ok`] is the expected response.
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub enum Command {
105 /// Stop the `daemon` instance.
106 ///
107 /// After sending this command, the `daemon` will close almost immediately, so all connections
108 /// to it should be considered closed.
109 Shutdown,
110 /// Subcommand for managing the music library.
111 Library(LibraryCommand),
112 /// Stream [events](Event) from the `daemon`.
113 ///
114 /// The daemon will stop accepting requests from the connection this command was executed on.
115 EventStream,
116 /// Close the connection to the `daemon`.
117 Disconnect,
118 /// Command to the playback module.
119 Playback(PlaybackCommand),
120 /// Ping the `daemon`.
121 ///
122 /// The `daemon` will respond to this with [`Response::Pong`] if successful.
123 Ping,
124}
125
126/// Defines the socket type to use when attempting to connect to a `daemon`.
127#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
128pub enum SocketType {
129 /// Only use a namespaced socket with no fallback.
130 ///
131 /// The connection will fail if a namespaced socket with the specified name doesn't exist.
132 NamespacedOnly,
133 /// Use a namespaced socket when possible, but allow a fallback to a filesystem socket.
134 ///
135 /// The connection will fail if there are no namespaced or filesystem sockets with the
136 /// specified name.
137 #[default]
138 NamespacedOrFilesystem,
139 /// Only use a filesystem socket with no fallback.
140 ///
141 /// The connection will fail if a filesystem socket with the specified name doesn't exist.
142 FilesystemOnly,
143}
144
145/// Returns a filesystem path for the given socket name.
146fn get_fs_socket_path(socket_name: &str) -> PathBuf {
147 let mut temp_dir = temp_dir();
148 temp_dir.push(socket_name);
149 temp_dir
150}
151
152/// Attempts to get a socket address for `daemon` IPC.
153///
154/// # Examples
155/// ```
156/// match chilen_ipc::get_socket(
157/// chilen_ipc::DEFAULT_SOCKET_NAME,
158/// &chilen_ipc::SocketType::NamespacedOrFilesystem
159/// ) {
160/// Ok(socket) => eprintln!("Got a socket: {socket:?}"),
161/// Err(e) => panic!("Could not obtain a socket: {e}"),
162/// }
163/// ```
164pub fn get_socket<'a>(socket_name: &'a str, mode: &SocketType) -> Result<Name<'a>, std::io::Error> {
165 match mode {
166 SocketType::NamespacedOnly => socket_name.to_ns_name::<GenericNamespaced>(),
167 SocketType::FilesystemOnly => {
168 get_fs_socket_path(socket_name).to_fs_name::<GenericFilePath>()
169 }
170 SocketType::NamespacedOrFilesystem => match socket_name.to_ns_name::<GenericNamespaced>() {
171 Ok(socket) => Ok(socket),
172 Err(e) => {
173 info!("Could not obtain a namespaced socket: {e}");
174 info!("Trying a filesystem socket instead");
175 get_fs_socket_path(socket_name).to_fs_name::<GenericFilePath>()
176 }
177 },
178 }
179}
180
181/// Serialize a client command to a format that can be sent to the daemon.
182///
183/// This uses the [`rmp_serde`] crate under the hood.
184///
185/// # Examples
186///
187/// Connect to the daemon and immediately disconnect.
188/// ```no_run
189/// # use std::io::{BufReader, Write};
190/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, serialize_command, Command, Error};
191/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
192/// let mut conn = BufReader::new(conn);
193/// let cmd = serialize_command(&Command::Disconnect).unwrap();
194/// conn.get_mut().write_all(&cmd).unwrap();
195/// ```
196pub fn serialize_command(cmd: &Command) -> Result<Vec<u8>, Error> {
197 let mut data = Vec::new();
198 if let Err(e) = cmd.serialize(&mut rmp_serde::Serializer::new(&mut data)) {
199 error!("Could not encode the client command: {e}");
200 return Err(Error::EncodingError);
201 }
202 Ok(data)
203}
204
205/// Receive a daemon response from a buffered stream connection.
206///
207/// This function will block until a response is received or the connection is dropped.
208///
209/// # Examples
210/// ```no_run
211/// # use std::io::{BufReader, Write};
212/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, serialize_command, Command, receive_response};
213/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
214/// let mut conn = BufReader::new(conn);
215/// let cmd = serialize_command(&Command::EventStream).unwrap();
216/// conn.get_mut().write_all(&cmd).unwrap();
217/// loop {
218/// let response = receive_response(&mut conn).unwrap();
219/// println!("Got a response from the daemon: {response:?}");
220/// }
221/// ```
222pub fn receive_response(conn: &mut BufReader<Stream>) -> Result<Response, Error> {
223 match rmp_serde::from_read(conn) {
224 Ok(response) => Ok(response),
225 Err(e) => {
226 error!("Failed decoding a daemon response: {e}");
227 Err(Error::DecodingError)
228 }
229 }
230}
231
232/// Disconnects from the `daemon` by sending the [`Command::Disconnect`] command.
233///
234/// This is a convenience function, its effect could be achieved using utilities already provided
235/// in this crate.
236///
237/// # Examples
238/// ```no_run
239/// # use std::io::BufReader;
240/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, disconnect};
241/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
242/// let mut conn = BufReader::new(conn);
243///
244/// // Do some stuff with the connection here...
245///
246/// match disconnect(&mut conn) {
247/// Ok(_) => eprintln!("Disconnected from the daemon!"),
248/// Err(e) => panic!("Could not close the daemon connection: {e}"),
249/// }
250/// ```
251pub fn disconnect(conn: &mut BufReader<Stream>) -> Result<(), Error> {
252 trace!("Closing connection with the daemon");
253
254 let data = serialize_command(&Command::Disconnect)?;
255
256 match conn.get_mut().write_all(&data) {
257 Ok(_) => {}
258 Err(e) => {
259 error!("Failed sending the command to the daemon: {e}");
260 return Err(Error::SendingError);
261 }
262 }
263
264 let response = receive_response(conn)?;
265
266 match response {
267 Response::Error(e) => {
268 error!("Could not close the connection to the daemon: {e}");
269 return Err(e);
270 }
271 Response::Ok => {
272 trace!("Connection with the daemon closed");
273 }
274 _ => {
275 error!("Got an unexpected response while closing the daemon connection: {response:?}");
276 return Err(Error::InvalidResponse);
277 }
278 }
279
280 Ok(())
281}
282
283/// Connects to the `daemon` via a local socket and returns the connection [`Stream`].
284///
285/// # Examples
286/// ```no_run
287/// # use std::io::BufReader;
288/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, disconnect};
289/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
290/// let mut conn = BufReader::new(conn);
291/// eprintln!("Connected to the daemon: {conn:?}");
292/// // Run all your commands here!
293/// disconnect(&mut conn).unwrap();
294/// ```
295pub fn connect(socket_name: &str, socket_type: &SocketType) -> Result<Stream, Error> {
296 trace!("Connecting to daemon on socket '{socket_name}'");
297
298 let socket = match get_socket(socket_name, socket_type) {
299 Ok(sock) => sock,
300 Err(e) => {
301 error!("Could not obtain a socket: {e}");
302 return Err(Error::SocketError(e.to_string()));
303 }
304 };
305
306 let conn = match Stream::connect(socket) {
307 Ok(conn) => conn,
308 Err(e) => {
309 error!("Could not initialize a connection to the daemon: {e}");
310 return Err(Error::ConnectionError);
311 }
312 };
313
314 Ok(conn)
315}
316
317/// Executes a single `daemon` command on a new connection and closes it.
318///
319/// **Warning:** if this function returns the [`Ok`] variant, this only means that the command was
320/// successfully delivered to the `daemon`. It doesn't necessarily mean it was executed
321/// successfully on the `daemon` side.
322///
323/// # Examples
324/// ```no_run
325/// # use chilen_ipc::{send_command, Command, DEFAULT_SOCKET_NAME, SocketType, Response};
326/// match send_command(Command::Ping, DEFAULT_SOCKET_NAME, &SocketType::default()) {
327/// // The `Ok` variant only means the command was delivered
328/// Ok(response) => {
329/// match response {
330/// Response::Ok => eprintln!("Got an `Ok` response, all good!"),
331/// // Depending on the type of command sent, the response from the daemon may be different.
332/// _ => panic!("Got an unexpected response: {response:?}"),
333/// }
334/// },
335/// Err(error) => panic!("Could not send a command to the daemon: {error}"),
336/// }
337/// ```
338pub fn send_command(
339 cmd: Command,
340 socket_name: &str,
341 socket_type: &SocketType,
342) -> Result<Response, Error> {
343 trace!("Executing daemon command: {cmd:?}");
344
345 let mut conn = match connect(socket_name, socket_type) {
346 Ok(conn) => BufReader::new(conn),
347 Err(e) => {
348 error!("{e}");
349 return Err(e);
350 }
351 };
352
353 let data = serialize_command(&cmd)?;
354
355 if let Err(e) = conn.get_mut().write_all(&data) {
356 error!("Failed sending the daemon command: {e}");
357 return Err(Error::SendingError);
358 }
359
360 let response = receive_response(&mut conn)?;
361
362 if cmd == Command::Shutdown {
363 trace!(
364 "Not trying to close the connection to the daemon, it will likely shut down by then"
365 );
366 } else if let Err(e) = disconnect(&mut conn) {
367 error!("Could not close the connection to the daemon: {e}");
368 }
369
370 Ok(response)
371}