Skip to main content

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}