Skip to main content

chilen_daemon/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_auto_cfg))]
3#![feature(doc_cfg)]
4
5mod daemon_thread;
6mod music_lib;
7pub mod playback;
8#[cfg(test)]
9mod tests;
10
11use std::{
12    env::{home_dir, temp_dir},
13    fs::remove_file,
14    path::PathBuf,
15    sync::{
16        Arc, LazyLock, RwLock,
17        mpsc::{self, channel},
18    },
19    thread,
20};
21
22use interprocess::local_socket::{Listener, ListenerOptions, Stream, traits::ListenerExt};
23use log::{debug, error, info, trace, warn};
24
25use chilen_ipc::{Command, DEFAULT_SOCKET_NAME, Event, Response, send_command};
26use serde::{Deserialize, Serialize};
27
28use crate::music_lib::{CACHE_DIR, covers::LoadMode};
29
30/// Defines the socket type to use when starting the daemon.
31pub type SocketType = chilen_ipc::SocketType;
32
33static EVENT_SENDER: LazyLock<Arc<RwLock<Option<mpsc::Sender<Event>>>>> =
34    LazyLock::new(|| Arc::new(RwLock::new(None)));
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum Error {
38    /// Could not obtain the daemon socket address.
39    SocketError,
40    /// The socket address is already in use.
41    AddrInUse,
42    /// Emitted when the daemon event channel is already initialized when starting the daemon.
43    ///
44    /// This likely means a second daemon was started in the same context.
45    EventChannelInitialized,
46    /// The event channel is not initialized, which likely means that there is no daemon running.
47    DaemonNotRunning,
48    NoLibrary,
49    LibraryNotAccessible,
50    CacheDirError(String),
51    DataDirError(String),
52    ConfigNotInitialized,
53}
54
55impl std::fmt::Display for Error {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::SocketError => write!(f, "Socket creation/connection failed"),
59            Self::AddrInUse => write!(f, "The socket address is already in use"),
60            Self::EventChannelInitialized => {
61                write!(f, "The event channel for the daemon is already initialized")
62            }
63            Self::DaemonNotRunning => write!(f, "The daemon doesn't seem to be running"),
64            Self::NoLibrary => {
65                write!(f, "The provided music library directory does not exist")
66            }
67            Self::LibraryNotAccessible => write!(
68                f,
69                "Could not access the music library due to a permission issue"
70            ),
71            Self::CacheDirError(e) => write!(f, "Could not initialize the cache directory: {e}"),
72            Self::DataDirError(e) => write!(f, "Could not initialize the data directory: {e}"),
73            Self::ConfigNotInitialized => write!(f, "The daemon configuration is not set"),
74        }
75    }
76}
77
78/// Defines under which conditions should the daemon claim an occupied socket address.
79///
80/// This is only effective if filesystem sockets are used.
81#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub enum AddrClaimMode {
83    /// Do not claim the socket address under any circumstances.
84    DoNotClaim,
85    /// Only claim the socket address if there is no response to ping commands from the process
86    /// that listens on the socket.
87    #[default]
88    ClaimIfUnresponsive,
89    /// Force claim the socket address.
90    ForceClaim,
91}
92
93/// Error originating from the [`Config`] struct.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95pub enum ConfigError {
96    /// The provided bus name suffix for MPRIS was invalid.
97    InvalidBusNameSuffix,
98    /// Could not get the home directory path.
99    HomeError,
100}
101
102impl std::fmt::Display for ConfigError {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            Self::InvalidBusNameSuffix => write!(f, "The bus name suffix provided was invalid"),
106            Self::HomeError => write!(f, "Could not get the home directory path"),
107        }
108    }
109}
110
111/// Configuration options for the daemon.
112///
113/// Used with the [`start`] function.
114#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
115pub struct Config {
116    /// The directory containing daemon cache.
117    pub cache_dir: PathBuf,
118    /// The directory containing the program data, eg. the playlist file.
119    pub data_dir: PathBuf,
120    /// The directory containing the music library/audio files to load.
121    pub music_dir: PathBuf,
122    /// The name of the socket to listen on.
123    ///
124    /// This will either resolve to a namespaced socket with the same name, or, in case of a
125    /// filesystem socket, a file with the same name in the temporary directory.
126    pub socket_name: String,
127    /// Defines under which conditions should the daemon claim an occupied socket address.
128    pub addr_claim_mode: AddrClaimMode,
129    /// Defines the type of socket the daemon should use.
130    pub socket_type: SocketType,
131    /// Configuration options specific to the [`playback`] module.
132    pub playback_config: playback::Config,
133}
134
135impl Config {
136    /// Get the default daemon config.
137    ///
138    /// For a custom music player, you probably want to create your own config from
139    /// scratch, or use the [`Config::try_from_name`] constructor.
140    ///
141    /// # Examples
142    /// ```
143    /// # use chilen_daemon;
144    /// let conf = chilen_daemon::Config::try_default().unwrap();
145    /// ```
146    pub fn try_default() -> Result<Self, ConfigError> {
147        Self::try_from_name("my-player", DEFAULT_SOCKET_NAME)
148    }
149
150    /// Create a new config from program and socket names.
151    ///
152    /// The generated paths will contain the name of the program, eg. `~/.cache/<PROGRAM_NAME>`,
153    /// `~/.local/share/<PROGRAM_NAME>`.
154    ///
155    /// Fails if the path to the home directory cannot be obtained.
156    ///
157    /// # Examples
158    /// ```
159    /// # use chilen_daemon;
160    /// let conf = chilen_daemon::Config::try_from_name("my-player", "MY_PLAYER");
161    /// ```
162    pub fn try_from_name(name: &str, socket_name: &str) -> Result<Self, ConfigError> {
163        let home_dir = match home_dir() {
164            Some(home) => home,
165            None => {
166                return Err(ConfigError::HomeError);
167            }
168        };
169
170        let mut cache_dir = home_dir.clone();
171        cache_dir.push(".cache");
172        cache_dir.push(name);
173
174        let mut data_dir = home_dir.clone();
175        data_dir.push(".local/share");
176        data_dir.push(name);
177
178        let mut music_dir = home_dir.clone();
179        music_dir.push("Music");
180
181        #[cfg(feature = "mpris")]
182        let bus_name_suffix = String::from("com.dev.") + name;
183        #[cfg(feature = "mpris")]
184        if !bus_name_suffix.is_ascii() {
185            return Err(ConfigError::InvalidBusNameSuffix);
186        }
187
188        Ok(Config {
189            cache_dir,
190            music_dir,
191            data_dir,
192            socket_name: socket_name.to_string(),
193            addr_claim_mode: AddrClaimMode::default(),
194            socket_type: SocketType::default(),
195            playback_config: playback::Config {
196                #[cfg(feature = "mpris")]
197                identity: name.to_string(),
198                #[cfg(feature = "mpris")]
199                bus_name_suffix,
200                allow_rate_modification: false,
201                #[cfg(feature = "mpris")]
202                can_raise: false,
203            },
204        })
205    }
206}
207
208fn handle_error(conn: std::io::Result<Stream>) -> Option<Stream> {
209    match conn {
210        Ok(c) => Some(c),
211        Err(e) => {
212            warn!("Incoming connection failed: {e}");
213            None
214        }
215    }
216}
217
218/// Returns a filesystem path for the given socket name.
219fn get_fs_socket_path(socket_name: &str) -> PathBuf {
220    let mut temp_dir = temp_dir();
221    temp_dir.push(socket_name);
222    temp_dir
223}
224
225/// Create an IPC socket listener for the daemon with the specified address.
226///
227/// Depending on the configuration, this can either return a namespaced socket or a filesystem one.
228///
229/// A filesystem socket will be returned if namespaced sockets are not supported, and the
230/// `socket_type` value passed is [`SocketType::NamespacedOrFilesystem`], or if the `socket_type`
231/// value passed is [`SocketType::FilesystemOnly`].
232///
233/// The [`AddrClaimMode`] value defines under which conditions should an occupied socket address be
234/// claimed. This is only effective for filesystem sockets.
235fn get_listener(
236    socket_name: &str,
237    socket_type: &SocketType,
238    claim_mode: &AddrClaimMode,
239) -> Result<Listener, Error> {
240    let socket = match chilen_ipc::get_socket(socket_name, socket_type) {
241        Ok(sock) => sock,
242        Err(e) => {
243            error!("Could not obtain the socket: {e}");
244            return Err(Error::SocketError);
245        }
246    };
247
248    let opts = ListenerOptions::new().name(socket.clone());
249    if socket.is_namespaced() {
250        trace!(
251            "Creating a listener on \"{}\" (namespaced socket)",
252            socket_name
253        );
254    } else {
255        trace!(
256            "Creating a listener on \"{}\" (filesystem socket)",
257            socket_name
258        );
259    }
260
261    match opts.create_sync() {
262        Ok(listener) => Ok(listener),
263        Err(e) => {
264            if e.kind() == std::io::ErrorKind::AddrInUse
265                && socket.is_path()
266                && !socket.is_namespaced()
267            {
268                warn!("The socket address is already in use");
269
270                match claim_mode {
271                    AddrClaimMode::DoNotClaim => {
272                        info!(
273                            "The daemon is configured not to reclaim the socket address, aborting"
274                        );
275                        Err(Error::AddrInUse)
276                    }
277                    AddrClaimMode::ClaimIfUnresponsive => {
278                        info!("Attempting to claim the socket address");
279                        match send_command(Command::Ping, socket_name, socket_type) {
280                            Ok(response) => {
281                                if response == Response::Pong {
282                                    error!(
283                                        "The other daemon responded to the pong command, aborting"
284                                    );
285                                    return Err(Error::AddrInUse);
286                                } else {
287                                    error!(
288                                        "Got an unexpected response from the daemon: {response:?}"
289                                    );
290                                    return Err(Error::AddrInUse);
291                                }
292                            }
293                            Err(e) => {
294                                if e == chilen_ipc::Error::ConnectionError {
295                                    info!(
296                                        "The other daemon is either dead or unresponsive, claiming the address"
297                                    );
298                                } else {
299                                    error!(
300                                        "Got an unexpected error while sending a ping command: {e}"
301                                    );
302                                    return Err(Error::AddrInUse);
303                                }
304                            }
305                        }
306
307                        if let Err(e) = remove_file(get_fs_socket_path(socket_name)) {
308                            error!("Could not remove the old socket: {e}");
309                            return Err(Error::SocketError);
310                        }
311                        let opts = ListenerOptions::new().name(socket.clone());
312                        match opts.create_sync() {
313                            Ok(listener) => {
314                                info!("Successfully claimed the address");
315                                Ok(listener)
316                            }
317                            Err(e) => {
318                                error!("Could not create a listener: {e}");
319                                Err(Error::SocketError)
320                            }
321                        }
322                    }
323                    AddrClaimMode::ForceClaim => {
324                        info!("Force claiming the socket address");
325                        if let Err(e) = remove_file(get_fs_socket_path(socket_name)) {
326                            error!("Could not remove the old socket: {e}");
327                            return Err(Error::SocketError);
328                        }
329                        let opts = ListenerOptions::new().name(socket.clone());
330                        match opts.create_sync() {
331                            Ok(listener) => {
332                                info!("Successfully claimed the address");
333                                Ok(listener)
334                            }
335                            Err(e) => {
336                                error!(
337                                    "Could not create a listener despite claiming the socket: {e}"
338                                );
339                                Err(Error::SocketError)
340                            }
341                        }
342                    }
343                }
344            } else {
345                error!("Failed creating a listener for the daemon socket: {e}");
346                Err(Error::SocketError)
347            }
348        }
349    }
350}
351
352/// Send an event to the daemon thread.
353///
354/// Will always return an error when the daemon isn't initialized, for example during testing.
355pub(crate) fn send_event(event: Event) -> Result<(), String> {
356    match EVENT_SENDER.read().as_mut() {
357        Ok(guard) => match guard.clone() {
358            Some(guard) => match guard.send(event) {
359                Ok(_) => Ok(()),
360                Err(e) => {
361                    error!("Could not send the event to the daemon: {e}");
362                    Err(e.to_string())
363                }
364            },
365            None => Err(String::from(
366                "Could not obtain the event channel, this is expected during testing",
367            )),
368        },
369        Err(e) => Err(e.to_string()),
370    }
371}
372
373/// Set whether clients can send raise requests to the daemon.
374///
375/// Will fail with [`Error::ConfigNotInitialized`] if the daemon isn't running.
376#[cfg(any(feature = "mpris", doc))]
377pub fn set_can_raise(can_raise: bool) -> Result<(), Error> {
378    use crate::playback::CONFIG;
379
380    let mut conf_guard = CONFIG.write().unwrap();
381    let conf = match conf_guard.as_mut() {
382        Some(conf) => conf,
383        None => return Err(Error::ConfigNotInitialized),
384    };
385    conf.can_raise = can_raise;
386    Ok(())
387}
388
389/// Stop a running daemon instance.
390///
391/// This has the same effect as sending a [`Command::Shutdown`] to the `daemon`, but it bypasses
392/// the requirement to connect to it over a local socket.
393///
394/// If a daemon is not running, [`Error::DaemonNotRunning`] will be returned.
395pub fn stop() -> Result<(), Error> {
396    if send_event(Event::Shutdown).is_err() {
397        error!("The daemon doesn't seem to be running");
398        Err(Error::DaemonNotRunning)
399    } else {
400        Ok(())
401    }
402}
403
404/// Start the daemon with the given config.
405///
406/// The `daemon` usually starts listening for commands around 100ms after this function is
407/// called on a low-end system, but some commands sent too early might fail if the music library
408/// isn't loaded yet, the playback module is not initialized, or if the MPRIS server hasn't started
409/// yet (if the MPRIS feature is enabled).
410///
411/// The initialization of the music library is by far the most time-consuming process ran when the
412/// `daemon` starts, and the time it takes vastly depends on the read speeds of the hard drive of
413/// the host machine and the size of the music library.
414///
415/// **Note:** This function will block. Launch it in a separate [`thread`] if you want to run the
416/// `daemon` in the background.
417///
418/// # Examples
419/// ```no_run
420/// # use chilen_daemon;
421/// let config = chilen_daemon::Config::try_default().unwrap();
422/// chilen_daemon::start(config).unwrap();
423/// ```
424pub fn start(config: Config) -> Result<(), Error> {
425    debug!("Starting daemon on \"{}\"", config.socket_name);
426
427    if let Err(e) = music_lib::set_dirs(config.clone()) {
428        error!("Could not set the paths: {e}");
429        return Err(e);
430    }
431
432    if config.socket_name == chilen_ipc::DEFAULT_SOCKET_NAME {
433        warn!(
434            "Using the default IPC socket name. Please use a unique name outside of just testing"
435        );
436    }
437
438    let listener = get_listener(
439        &config.socket_name,
440        &config.socket_type,
441        &config.addr_claim_mode,
442    )?;
443
444    thread::spawn(move || {
445        let _ = music_lib::state::load(LoadMode::Load);
446        playback::init(config.playback_config);
447    });
448
449    let senders = Arc::new(RwLock::new(Vec::new()));
450
451    thread::spawn({
452        let senders_clone = senders.clone();
453        info!("Listening for incoming connections");
454        move || {
455            for (index, conn) in listener.incoming().filter_map(handle_error).enumerate() {
456                let (ttx, trx) = channel();
457                senders_clone.clone().write().unwrap().push(ttx);
458                // Size for `u64` is 8 bytes, and for `usize` it's 4 bytes on 32-bit and 8
459                // bytes on 64-bit, so the conversion can be done safely if I understand this
460                // correctly :)
461                daemon_thread::spawn(conn, trx, u64::try_from(index).unwrap());
462            }
463        }
464    });
465
466    let mut guard = EVENT_SENDER.write().unwrap();
467    // I could check if the sender is still connected here
468    if guard.as_ref().is_some() {
469        error!("Could not start the daemon because the event channel is already initialized");
470        info!("(Did you try to start a second daemon in the same context?)");
471        return Err(Error::EventChannelInitialized);
472    }
473    let (event_sender, event_receiver) = mpsc::channel();
474    *guard = Some(event_sender);
475    drop(guard);
476
477    loop {
478        // Launching a different daemon overwrites the [EVENT_SENDER] variable, causing the
479        // previous sender to be dropped and the `recv()` method to return an [Err] type.
480        let event = event_receiver.recv().unwrap();
481        let mut guard = senders.write().unwrap();
482        let mut dead = Vec::new();
483        for (i, sender) in guard.iter().enumerate() {
484            if sender.send(event.clone()).is_err() {
485                debug!("Removing dead connection at {}", i);
486                dead.push(i);
487            }
488        }
489        for &ded in dead.iter().rev() {
490            guard.remove(ded);
491        }
492
493        match event {
494            Event::Shutdown => {
495                trace!("Received shutdown event");
496                let mut guard = EVENT_SENDER.write().unwrap();
497                *guard = None;
498                info!("Stopped.");
499                std::process::exit(0);
500            }
501            Event::ConnectionClosed => {
502                trace!("Connection with a client closed")
503            }
504            _ => {}
505        }
506    }
507}