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::{Arc, LazyLock, RwLock, mpsc},
16 thread::{self, JoinHandle},
17};
18
19use interprocess::local_socket::{Listener, ListenerOptions, Stream, traits::ListenerExt};
20use log::{debug, error, info, trace, warn};
21
22use chilen_ipc::{Command, DEFAULT_SOCKET_NAME, Event, Response};
23use serde::{Deserialize, Serialize};
24
25use crate::{
26 daemon_thread::ThreadCommand,
27 music_lib::{
28 CACHE_DIR,
29 covers::LoadMode,
30 state::{self, get_library},
31 },
32};
33
34pub type SocketType = chilen_ipc::SocketType;
36
37static EVENT_SENDERS: LazyLock<Arc<RwLock<Vec<mpsc::Sender<Event>>>>> =
38 LazyLock::new(|| Arc::new(RwLock::new(Vec::new())));
39
40static COMMAND_SENDER: LazyLock<Arc<RwLock<Option<mpsc::Sender<ThreadCommand>>>>> =
42 LazyLock::new(|| Arc::new(RwLock::new(None)));
43
44pub(crate) static CONFIG: LazyLock<Arc<RwLock<Option<Config>>>> =
47 LazyLock::new(|| Arc::new(RwLock::new(None)));
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub enum Error {
51 SocketError,
53 AddrInUse,
55 EventChannelInitialized,
59 DaemonNotRunning,
61 NoLibrary,
63 LibraryNotAccessible,
65 CacheDirError(String),
69 DataDirError(String),
73 QuitDisabled,
75 RaiseDisabled,
77 SetFullscreenDisabled,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
83pub enum Request {
84 Raise,
87 SetFullscreen(bool),
89}
90
91impl std::fmt::Display for Error {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Self::SocketError => write!(f, "Socket creation failed"),
95 Self::AddrInUse => write!(f, "The socket address is already in use"),
96 Self::EventChannelInitialized => {
97 write!(f, "The event channel is already initialized")
98 }
99 Self::DaemonNotRunning => write!(f, "The daemon doesn't seem to be running"),
100 Self::NoLibrary => {
101 write!(
102 f,
103 "The provided music library path is not a directory or doesn't exist"
104 )
105 }
106 Self::LibraryNotAccessible => write!(
107 f,
108 "Could not access the music library due to a permission issue"
109 ),
110 Self::CacheDirError(e) => write!(f, "Could not initialize the cache directory: {e}"),
111 Self::DataDirError(e) => write!(f, "Could not initialize the data directory: {e}"),
112 Self::QuitDisabled => write!(f, "Quit requests from external clients are not allowed"),
113 Self::RaiseDisabled => write!(f, "Raise requests are not allowed"),
114 Self::SetFullscreenDisabled => write!(
115 f,
116 "Toggling fullscreen mode by external clients is not allowed"
117 ),
118 }
119 }
120}
121
122#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
126pub enum AddrClaimMode {
127 DoNotClaim,
129 #[default]
132 ClaimIfUnresponsive,
133 ForceClaim,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
139pub enum ConfigError {
140 InvalidBusNameSuffix,
142 HomeError,
144}
145
146impl std::fmt::Display for ConfigError {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 match self {
149 Self::InvalidBusNameSuffix => write!(f, "The bus name suffix provided was invalid"),
150 Self::HomeError => write!(f, "Could not get the home directory path"),
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
160pub struct Config {
161 pub cache_dir: PathBuf,
163 pub data_dir: PathBuf,
165 pub music_dir: PathBuf,
167 pub socket_name: String,
172 pub addr_claim_mode: AddrClaimMode,
174 pub socket_type: SocketType,
176 pub can_raise: bool,
182 pub can_set_fullscreen: bool,
184 pub can_quit: bool,
189 #[cfg(feature = "mpris")]
192 pub desktop_entry: Option<String>,
193 pub playback_config: playback::Config,
195}
196
197impl Config {
198 pub fn try_default() -> Result<Self, ConfigError> {
209 Self::try_from_name("my-player", DEFAULT_SOCKET_NAME)
210 }
211
212 pub fn try_from_name(name: &str, socket_name: &str) -> Result<Self, ConfigError> {
225 let home_dir = match home_dir() {
226 Some(home) => home,
227 None => {
228 return Err(ConfigError::HomeError);
229 }
230 };
231
232 let mut cache_dir = home_dir.clone();
233 cache_dir.push(".cache");
234 cache_dir.push(name);
235
236 let mut data_dir = home_dir.clone();
237 data_dir.push(".local/share");
238 data_dir.push(name);
239
240 let mut music_dir = home_dir.clone();
241 music_dir.push("Music");
242
243 #[cfg(feature = "mpris")]
244 let bus_name_suffix = String::from("com.dev.") + name;
245 #[cfg(feature = "mpris")]
246 if !bus_name_suffix.is_ascii() {
247 return Err(ConfigError::InvalidBusNameSuffix);
248 }
249
250 Ok(Config {
251 cache_dir,
252 music_dir,
253 data_dir,
254 socket_name: socket_name.to_string(),
255 addr_claim_mode: AddrClaimMode::default(),
256 socket_type: SocketType::default(),
257 can_raise: false,
258 can_quit: true,
259 can_set_fullscreen: false,
260 #[cfg(feature = "mpris")]
261 desktop_entry: None,
262 playback_config: playback::Config {
263 #[cfg(feature = "mpris")]
264 identity: name.to_string(),
265 #[cfg(feature = "mpris")]
266 bus_name_suffix,
267 allow_rate_modification: false,
268 },
269 })
270 }
271}
272
273fn cleanup() {
275 trace!("Cleaning up...");
276 music_lib::cleanup(); state::cleanup();
278 playback::cleanup();
279 playback::state::cleanup();
280 *COMMAND_SENDER.write().unwrap() = None;
281 *EVENT_SENDERS.write().unwrap() = Vec::new();
282 *CONFIG.write().unwrap() = None;
283 trace!("Done cleaning up");
284}
285
286fn handle_error(conn: std::io::Result<Stream>) -> Option<Stream> {
287 match conn {
288 Ok(c) => Some(c),
289 Err(e) => {
290 warn!("Incoming connection failed: {e}");
291 None
292 }
293 }
294}
295
296fn get_fs_socket_path(socket_name: &str) -> PathBuf {
298 let mut temp_dir = temp_dir();
299 temp_dir.push(socket_name);
300 temp_dir
301}
302
303fn get_listener(
314 socket_name: &str,
315 socket_type: &SocketType,
316 claim_mode: &AddrClaimMode,
317) -> Result<Listener, Error> {
318 let socket = match chilen_ipc::get_socket(socket_name, socket_type) {
319 Ok(sock) => sock,
320 Err(e) => {
321 error!("Could not obtain the socket: {e}");
322 return Err(Error::SocketError);
323 }
324 };
325
326 let opts = ListenerOptions::new().name(socket.clone());
327 if socket.is_namespaced() {
328 trace!(
329 "Creating a listener on \"{}\" (namespaced socket)",
330 socket_name
331 );
332 } else {
333 trace!(
334 "Creating a listener on \"{}\" (filesystem socket)",
335 socket_name
336 );
337 }
338
339 match opts.create_sync() {
340 Ok(listener) => Ok(listener),
341 Err(e) => {
342 if e.kind() == std::io::ErrorKind::AddrInUse
343 && socket.is_path()
344 && !socket.is_namespaced()
345 {
346 warn!("The socket address is already in use");
347
348 match claim_mode {
349 AddrClaimMode::DoNotClaim => {
350 info!(
351 "The daemon is configured not to reclaim the socket address, aborting"
352 );
353 Err(Error::AddrInUse)
354 }
355 AddrClaimMode::ClaimIfUnresponsive => {
356 info!("Attempting to claim the socket address");
357 match chilen_ipc::send_command(Command::Ping, socket_name, socket_type) {
358 Ok(response) => {
359 if response == Response::Pong {
360 error!(
361 "The other daemon responded to the pong command, aborting"
362 );
363 return Err(Error::AddrInUse);
364 } else {
365 error!(
366 "Got an unexpected response from the daemon: {response:?}"
367 );
368 return Err(Error::AddrInUse);
369 }
370 }
371 Err(e) => {
372 if e == chilen_ipc::Error::ConnectionError {
373 info!(
374 "The other daemon is either dead or unresponsive, claiming the address"
375 );
376 } else {
377 error!(
378 "Got an unexpected error while sending a ping command: {e}"
379 );
380 return Err(Error::AddrInUse);
381 }
382 }
383 }
384
385 if let Err(e) = remove_file(get_fs_socket_path(socket_name)) {
386 error!("Could not remove the old socket: {e}");
387 return Err(Error::SocketError);
388 }
389 let opts = ListenerOptions::new().name(socket.clone());
390 match opts.create_sync() {
391 Ok(listener) => {
392 info!("Successfully claimed the address");
393 Ok(listener)
394 }
395 Err(e) => {
396 error!("Could not create a listener: {e}");
397 Err(Error::SocketError)
398 }
399 }
400 }
401 AddrClaimMode::ForceClaim => {
402 info!("Force claiming the socket address");
403 if let Err(e) = remove_file(get_fs_socket_path(socket_name)) {
404 error!("Could not remove the old socket: {e}");
405 return Err(Error::SocketError);
406 }
407 let opts = ListenerOptions::new().name(socket.clone());
408 match opts.create_sync() {
409 Ok(listener) => {
410 info!("Successfully claimed the address");
411 Ok(listener)
412 }
413 Err(e) => {
414 error!(
415 "Could not create a listener despite claiming the socket: {e}"
416 );
417 Err(Error::SocketError)
418 }
419 }
420 }
421 }
422 } else {
423 error!("Failed creating a listener for the daemon socket: {e}");
424 Err(Error::SocketError)
425 }
426 }
427 }
428}
429
430pub(crate) fn send_command(command: ThreadCommand) -> Result<(), String> {
432 let conf_guard = crate::CONFIG.read().unwrap();
433 let config = match conf_guard.as_ref() {
434 Some(conf) => conf,
435 None => return Err(Error::DaemonNotRunning.to_string()),
436 };
437 let sender_guard = COMMAND_SENDER.read().unwrap();
438 let sender = match sender_guard.as_ref() {
439 Some(sender) => sender,
440 None => return Err(Error::DaemonNotRunning.to_string()),
441 };
442 if command == ThreadCommand::Quit {
443 if !config.can_quit {
444 return Err(Error::QuitDisabled.to_string());
445 }
446 } else if command == ThreadCommand::Raise && !config.can_raise {
447 return Err(Error::RaiseDisabled.to_string());
448 }
449 if let Err(e) = sender.send(command) {
450 error!("Could not send the thread command to the daemon: {e}");
451 return Err(e.to_string());
452 }
453 Ok(())
454}
455
456pub(crate) fn send_event(event: Event) {
458 let mut senders = EVENT_SENDERS.write().unwrap();
459 let mut dead = Vec::new();
460 for (i, sender) in senders.iter().enumerate() {
461 if sender.send(event.clone()).is_err() {
462 info!("Removing dead event sender at {i}");
463 dead.push(i);
464 }
465 }
466 for ded in dead {
467 senders.swap_remove(ded);
468 }
469}
470
471pub(crate) fn subscribe_to_events() -> Result<mpsc::Receiver<Event>, chilen_ipc::Error> {
473 let mut events = Vec::new();
474 match get_library() {
475 Ok(lib) => {
476 events.push(Event::LibraryChanged(lib.into()));
477 }
478 Err(e) => {
479 error!("Could not get the contents of the music library: {e}");
480 return Err(e);
481 }
482 }
483 match playback::get_initial_events() {
484 Ok(playback_events) => {
485 for event in playback_events {
486 events.push(event);
487 }
488 }
489 Err(e) => {
490 error!("Could not get the initial events from the playback module: {e}");
491 return Err(e);
492 }
493 };
494 let guard = crate::CONFIG.read().unwrap();
495 let conf = guard.as_ref().unwrap();
496 events.push(Event::CanRaiseChanged(conf.can_raise));
497 events.push(Event::CanQuitChanged(conf.can_quit));
498 let (sender, receiver) = mpsc::channel();
499 for event in events {
500 sender.send(event).unwrap();
501 }
502 let mut guard = EVENT_SENDERS.write().unwrap();
503 let senders: &mut Vec<mpsc::Sender<Event>> = guard.as_mut();
504 senders.push(sender);
505 Ok(receiver)
506}
507
508pub(crate) fn raise() -> Result<(), Error> {
509 let guard = crate::CONFIG.read().unwrap();
510 if guard.as_ref().unwrap().can_raise {
511 match crate::send_command(ThreadCommand::Raise) {
512 Ok(_) => Ok(()),
513 Err(_) => Err(Error::RaiseDisabled),
514 }
515 } else {
516 Err(Error::RaiseDisabled)
517 }
518}
519
520pub(crate) fn set_fullscreen(fullscreen: bool) -> Result<(), Error> {
521 let guard = crate::CONFIG.read().unwrap();
522 if guard.as_ref().unwrap().can_set_fullscreen {
523 match crate::send_command(ThreadCommand::SetFullscreen(fullscreen)) {
524 Ok(_) => Ok(()),
525 Err(_) => Err(Error::SetFullscreenDisabled),
526 }
527 } else {
528 Err(Error::SetFullscreenDisabled)
529 }
530}
531
532pub fn set_can_raise(can_raise: bool) -> Result<(), Error> {
536 let mut conf_guard = CONFIG.write().unwrap();
537 let conf = match conf_guard.as_mut() {
538 Some(conf) => conf,
539 None => return Err(Error::DaemonNotRunning),
540 };
541 if conf.can_raise != can_raise {
542 conf.can_raise = can_raise;
543 send_event(Event::CanRaiseChanged(conf.can_raise));
544 }
545 Ok(())
546}
547
548pub fn set_can_set_fullscreen(can_set_fullscreen: bool) -> Result<(), Error> {
549 let mut conf_guard = CONFIG.write().unwrap();
550 let conf = match conf_guard.as_mut() {
551 Some(conf) => conf,
552 None => return Err(Error::DaemonNotRunning),
553 };
554 if conf.can_set_fullscreen != can_set_fullscreen {
555 conf.can_set_fullscreen = can_set_fullscreen;
556 send_event(Event::CanGoFullscreenChanged(conf.can_raise));
557 }
558 Ok(())
559}
560
561pub fn set_can_quit(can_quit: bool) -> Result<(), Error> {
567 let mut conf_guard = CONFIG.write().unwrap();
568 let conf = match conf_guard.as_mut() {
569 Some(conf) => conf,
570 None => return Err(Error::DaemonNotRunning),
571 };
572 if conf.can_quit != can_quit {
573 conf.can_quit = can_quit;
574 send_event(Event::CanQuitChanged(conf.can_quit));
575 }
576 Ok(())
577}
578
579pub fn quit() -> Result<(), Error> {
583 if send_command(ThreadCommand::Quit).is_err() {
584 error!("The daemon doesn't seem to be running");
585 Err(Error::DaemonNotRunning)
586 } else {
587 Ok(())
588 }
589}
590
591#[cfg(feature = "mpris")]
594pub(crate) fn client_quit() -> Result<(), Error> {
595 let guard = crate::CONFIG.read().unwrap();
596 if guard.as_ref().unwrap().can_quit {
597 quit().unwrap();
598 Ok(())
599 } else {
600 Err(Error::QuitDisabled)
601 }
602}
603
604pub fn start(config: Config) -> (mpsc::Receiver<Request>, JoinHandle<Result<(), Error>>) {
635 let (sender, receiver) = mpsc::channel();
636 (receiver, thread::spawn(|| start_blocking(sender, config)))
637}
638
639fn start_blocking(request_sender: mpsc::Sender<Request>, config: Config) -> Result<(), Error> {
641 debug!("Starting daemon on \"{}\"", config.socket_name);
642
643 *CONFIG.write().unwrap() = Some(config.clone());
644
645 if let Err(e) = music_lib::set_dirs(config.clone()) {
646 error!("Could not set the paths: {e}");
647 return Err(e);
648 }
649
650 if config.socket_name == chilen_ipc::DEFAULT_SOCKET_NAME {
651 warn!(
652 "Using the default IPC socket name. Please use a unique name outside of just testing"
653 );
654 }
655
656 let listener = get_listener(
657 &config.socket_name,
658 &config.socket_type,
659 &config.addr_claim_mode,
660 )?;
661
662 thread::spawn(move || {
663 let _ = music_lib::state::load(LoadMode::Load);
664 playback::init(config.playback_config);
665 });
666
667 thread::spawn({
668 move || {
669 for (index, conn) in listener.incoming().filter_map(handle_error).enumerate() {
670 daemon_thread::spawn(conn, u64::try_from(index).unwrap());
674 }
675 }
676 });
677
678 let mut sender_guard = COMMAND_SENDER.write().unwrap();
679 if sender_guard.as_ref().is_some() {
680 error!("The command channel is already initialized!");
681 return Err(Error::EventChannelInitialized);
682 }
683 let (sender, receiver) = mpsc::channel();
684 *sender_guard = Some(sender);
685 drop(sender_guard);
686
687 loop {
688 let cmd = receiver.recv().unwrap();
689 match cmd {
690 ThreadCommand::Raise => {
691 trace!("Received a raise request");
692 let _ = request_sender.send(Request::Raise);
693 }
694 ThreadCommand::SetFullscreen(fullscreen) => {
695 trace!("Received request to set fullscreen to: {fullscreen}");
696 let _ = request_sender.send(Request::SetFullscreen(fullscreen));
697 }
698 ThreadCommand::Quit => {
699 trace!("Received quit command");
700 send_event(Event::Quit);
701 cleanup();
702 info!("Stopped.");
703 return Ok(());
704 }
705 }
706 }
707}