use crate::audio::AudioEngine; use crate::config::Config; use crate::notification; use crate::queue::{Queue, ShuffleMode, Track}; use std::io::{Read, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; pub fn start_daemon() -> Result<(), Box> { let config = Config::load()?; let socket_path = config.get_socket_path(); let pid_path = get_pid_path(&config); // Check if daemon is already running if let Ok(pid) = check_running_daemon(&pid_path) { return Err(format!("Daemon already running with PID {}", pid).into()); } // Clean up stale socket file if it exists if socket_path.exists() { std::fs::remove_file(&socket_path).ok(); } // Create runtime directory if let Some(parent) = socket_path.parent() { std::fs::create_dir_all(parent)?; } if let Some(parent) = pid_path.parent() { std::fs::create_dir_all(parent)?; } // Write PID file let pid = std::process::id(); std::fs::write(&pid_path, pid.to_string())?; println!("Daemon started with PID {}", pid); // Shared shutdown flag - can be triggered by signal or command let shutdown_flag = Arc::new(AtomicBool::new(false)); let shutdown_flag_signal = Arc::clone(&shutdown_flag); // Handle SIGTERM and SIGINT ctrlc::set_handler(move || { println!("Received shutdown signal"); shutdown_flag_signal.store(true, Ordering::SeqCst); })?; let listener = UnixListener::bind(&socket_path)?; println!("Listening on {:?}", socket_path); let audio_engine = Arc::new(AudioEngine::new()); let queue = Arc::new(Mutex::new(Queue::new())); // Load and apply saved settings let config = Config::load().unwrap_or_default(); if let Some(vol) = config.volume { audio_engine.set_volume(vol as f32 / 100.0); } if let Some(ref shuffle_str) = config.shuffle && let Ok(mut q) = queue.lock() { match shuffle_str.as_str() { "on" | "all" => q.set_shuffle(ShuffleMode::All), "album" => q.set_shuffle(ShuffleMode::Album), _ => {} } } // Accept connections until shutdown listener.set_nonblocking(true)?; // Helper: poll until a specific track actually starts playing or timeout fn wait_for_start(audio: &AudioEngine, target: &PathBuf, timeout_ms: u64) -> bool { use std::time::{Duration, Instant}; let start = Instant::now(); while start.elapsed() < Duration::from_millis(timeout_ms) { if audio.is_playing() && let Some(cur) = audio.current_track() && &cur == target { return true; } std::thread::sleep(Duration::from_millis(10)); } false } loop { // Check for shutdown signal if shutdown_flag.load(Ordering::SeqCst) { break; } // Check if track ended and play next (robust with startup feedback) if audio_engine.poll_track_ended() && let Ok(mut q) = queue.lock() { let original_position = q.position(); let mut started = false; let mut had_next = false; let max_trials = 4usize; for _ in 0..max_trials { if let Some(next_track) = q.next() { had_next = true; audio_engine.play(next_track.clone()); if wait_for_start(&audio_engine, &next_track, 500) { started = true; notification::notify_track(&Track::from_path(next_track)); break; } } else { break; } } if !started { q.set_position(original_position); audio_engine.stop(); if had_next { notification::notify_error("Playback failed"); } else { notification::notify_queue_ended(); } } } match listener.accept() { Ok((stream, _)) => { let audio_clone = Arc::clone(&audio_engine); let shutdown_flag_clone = Arc::clone(&shutdown_flag); let queue_clone = Arc::clone(&queue); thread::spawn(move || { handle_client(stream, audio_clone, shutdown_flag_clone, queue_clone); }); } Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { thread::sleep(Duration::from_millis(50)); continue; } Err(e) => eprintln!("Connection failed: {}", e), } thread::sleep(Duration::from_millis(50)); } // Cleanup cleanup_daemon(&socket_path, &pid_path); println!("Daemon stopped"); Ok(()) } pub fn stop_daemon() -> Result<(), Box> { let config = Config::load()?; let pid_path = get_pid_path(&config); match check_running_daemon(&pid_path) { Ok(pid) => { // Try to send stop command first for graceful shutdown if let Ok(()) = send_command_raw("stop_daemon".to_string()) { // Wait for daemon to stop for _ in 0..50 { thread::sleep(Duration::from_millis(100)); if !is_process_running(pid) { break; } } } // Force kill if still running if is_process_running(pid) { unsafe { libc::kill(pid as i32, libc::SIGTERM); } thread::sleep(Duration::from_millis(500)); // Force kill if still alive if is_process_running(pid) { unsafe { libc::kill(pid as i32, libc::SIGKILL); } } } // Clean up files let socket_path = config.get_socket_path(); std::fs::remove_file(&socket_path).ok(); std::fs::remove_file(&pid_path).ok(); println!("Daemon stopped"); Ok(()) } Err(e) => Err(format!("Daemon not running: {}", e).into()), } } pub fn restart_daemon() -> Result<(), Box> { // Stop existing daemon if let Ok(()) = stop_daemon() { // Wait a bit for cleanup thread::sleep(Duration::from_millis(500)); } // Start new daemon in background let current_exe = std::env::current_exe()?; std::process::Command::new(current_exe) .arg("start") .spawn()?; println!("Daemon restarted"); Ok(()) } pub fn send_command(cmd: String) -> Result<(), Box> { send_command_raw(cmd)?; Ok(()) } fn send_command_raw(cmd: String) -> Result<(), Box> { let config = Config::load()?; let socket_path = config.get_socket_path(); let mut stream = UnixStream::connect(socket_path)?; stream.write_all(cmd.as_bytes())?; let mut response = String::new(); stream.read_to_string(&mut response)?; print!("{}", response); Ok(()) } fn handle_client( mut stream: UnixStream, audio: Arc, shutdown_flag: Arc, queue: Arc>, ) { let mut buffer = [0; 1024]; if let Ok(size) = stream.read(&mut buffer) { let cmd = String::from_utf8_lossy(&buffer[..size]); let response = process_command(cmd.trim(), &audio, &shutdown_flag, &queue); let _ = stream.write_all(response.as_bytes()); } } fn process_command( cmd: &str, audio: &AudioEngine, shutdown_flag: &Arc, queue: &Arc>, ) -> String { match cmd { "status" => { if audio.is_playing() { if let Some(track_path) = audio.current_track() { let track = Track::from_path(track_path); let track_str = track.display_string(0, 1); format!("Playing\nTrack: {}\n", track_str) } else { "Playing\n".to_string() } } else { "Stopped\n".to_string() } } "pause" => { audio.pause(); "Paused\n".to_string() } "resume" => { audio.resume(); "Resumed\n".to_string() } "toggle" => { if audio.is_playing() { audio.pause(); "Paused\n".to_string() } else { audio.resume(); "Resumed\n".to_string() } } "stop" => { audio.stop(); if let Ok(mut q) = queue.lock() { q.clear(); } "Stopped\n".to_string() } "stop_daemon" => { // Signal the daemon to shut down shutdown_flag.store(true, Ordering::SeqCst); "Shutting down\n".to_string() } "next" => { if let Ok(mut q) = queue.lock() { if let Some(next_track) = q.next() { audio.play(next_track.clone()); notification::notify_track(&Track::from_path(next_track)); "Playing next track\n".to_string() } else { audio.stop(); notification::notify_queue_ended(); "Queue empty, stopped\n".to_string() } } else { "Error accessing queue\n".to_string() } } "previous" => { if let Ok(mut q) = queue.lock() { if let Some(prev_track) = q.previous() { audio.play(prev_track.clone()); notification::notify_track(&Track::from_path(prev_track)); "Playing previous track\n".to_string() } else { "No previous track\n".to_string() } } else { "Error accessing queue\n".to_string() } } "shuffle" => { if let Ok(q) = queue.lock() { match q.shuffle_mode() { ShuffleMode::None => "Shuffle: off\n".to_string(), ShuffleMode::All => "Shuffle: on (all)\n".to_string(), ShuffleMode::Album => "Shuffle: on (album)\n".to_string(), } } else { "Error accessing queue\n".to_string() } } _ if cmd.starts_with("shuffle ") => { let mode_str = cmd[8..].trim(); if let Ok(mut q) = queue.lock() { let result = if mode_str == "on" || mode_str == "all" { q.set_shuffle(ShuffleMode::All); "Shuffle: on (all)\n".to_string() } else if mode_str == "album" { q.set_shuffle(ShuffleMode::Album); "Shuffle: on (album)\n".to_string() } else if mode_str == "off" || mode_str == "none" { q.set_shuffle(ShuffleMode::None); "Shuffle: off\n".to_string() } else { "Usage: shuffle [on|off|album]\n".to_string() }; // Save to config if let Ok(mut config) = Config::load() { if mode_str == "off" || mode_str == "none" { config.shuffle = None; } else { config.shuffle = Some(mode_str.to_string()); } let _ = config.save(); } result } else { "Error accessing queue\n".to_string() } } _ if cmd.starts_with("play ") => { let rest = &cmd[5..].trim(); let (position, path_str): (&str, &str) = if let Some((pos, p)) = rest.split_once(' ') { if pos == "now" || pos == "next" || pos == "last" { (pos, p) } else { ("last", rest) } } else { ("last", rest) }; let path_str = path_str.to_string(); if path_str.is_empty() { return "Usage: play [now|next|last] \n".to_string(); } let path = std::path::PathBuf::from(path_str.clone()); if path.is_file() { if let Ok(mut q) = queue.lock() { let was_empty = q.is_empty(); match position { "now" => { q.insert_after_current(path.clone()); if !was_empty { let new_pos = q.position() + 1; q.set_position(new_pos); } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); notification::notify_track(track); } format!("Playing now: {}\n", path_str) } "next" => { q.insert_after_current(path.clone()); format!("Queued next: {}\n", path_str) } _ => { let was_empty = q.is_empty(); let was_playing = audio.is_playing(); q.add_track(path.clone()); if was_empty && !was_playing { let track = Track::from_path(path.clone()); audio.play(path); notification::notify_track(&track); format!("Playing: {}\n", path_str) } else { format!("Queued: {}\n", path_str) } } } } else { "Error accessing queue\n".to_string() } } else if path.is_dir() { match find_audio_in_dir(&path) { Some(files) if !files.is_empty() => { if let Ok(mut q) = queue.lock() { let was_empty = q.is_empty(); match position { "now" => { for file in files.iter().rev() { q.insert_after_current(file.clone()); } if !was_empty { let new_pos = q.position() + 1; q.set_position(new_pos); } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); notification::notify_track(track); } format!( "Playing now: {} tracks from {}\n", files.len(), path_str ) } "next" => { for file in files.iter().rev() { q.insert_after_current(file.clone()); } format!( "Queued next: {} tracks from {}\n", files.len(), path_str ) } _ => { let was_playing = audio.is_playing(); for file in &files { q.add_track(file.clone()); } if was_empty && !was_playing && let Some(track) = q.current() { audio.play(track.path.clone()); notification::notify_track(track); } format!("Queued {} tracks from: {}\n", files.len(), path_str) } } } else { "Error accessing queue\n".to_string() } } _ => format!("Error: No audio files found in '{}'\n", path_str), } } else { format!("File not found: {}\n", path_str) } } _ if cmd == "volume" => { let vol = audio.get_volume(); format!("Volume: {}\n", vol) } _ if cmd.starts_with("volume ") => { if let Ok(level) = cmd[7..].trim().parse::() { audio.set_volume(level as f32 / 100.0); // Save to config if let Ok(mut config) = Config::load() { config.volume = Some(level); let _ = config.save(); } format!("Volume set to {}\n", level) } else { "Usage: volume <0-100>\n".to_string() } } _ if cmd.starts_with("queue") => { if let Ok(q) = queue.lock() { if q.is_empty() { "Queue is empty\n".to_string() } else { let mut output = String::new(); let current_position = q.position(); let playing_track_idx = match q.shuffle_mode() { ShuffleMode::None => current_position, _ => q .shuffled_indices() .get(current_position) .copied() .unwrap_or(0), }; let indices: Vec = if *q.shuffle_mode() == ShuffleMode::None { (0..q.len()).collect() } else { q.shuffled_indices().to_vec() }; let is_playing = audio.is_playing(); for (display_idx, &track_idx) in indices.iter().enumerate() { if let Some(track) = q.track_at(track_idx) { let track_str = track.display_string(display_idx, q.len()); if track_idx == playing_track_idx && is_playing { output.push_str(&format!("> {} (playing)\n", track_str)); } else { output.push_str(&format!(" {}\n", track_str)); } } } // Use pager if queue is longer than terminal height let terminal_rows = terminal_size::terminal_size() .map(|(w, _)| w.0 as usize) .unwrap_or(24); if output.lines().count() > terminal_rows { use std::process::{Command, Stdio}; let pager = Command::new("less").arg("-R").stdin(Stdio::piped()).spawn(); if let Ok(mut child) = pager { use std::io::Write; let _ = child.stdin.as_mut().unwrap().write_all(output.as_bytes()); let _ = child.wait(); "".to_string() } else { output } } else { output } } } else { "Error accessing queue\n".to_string() } } _ => "Unknown command\n".to_string(), } } fn find_audio_in_dir(dir: &Path) -> Option> { use std::ffi::OsStr; let mut results = Vec::new(); if let Ok(entries) = std::fs::read_dir(dir) { let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); entries.sort_by_key(|e| e.file_name()); for entry in entries { let path = entry.path(); if path.is_file() { if let Some(ext) = path.extension().and_then(OsStr::to_str) && matches!( ext.to_lowercase().as_str(), "mp3" | "flac" | "ogg" | "m4a" | "wav" | "opus" ) { results.push(path); } } else if path.is_dir() && let Some(mut found) = find_audio_in_dir(&path) { results.append(&mut found); } } } if results.is_empty() { None } else { Some(results) } } /// Get the PID file path for the daemon fn get_pid_path(_config: &Config) -> std::path::PathBuf { let runtime_dir = std::env::var("XDG_RUNTIME_DIR") .map(std::path::PathBuf::from) .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")); runtime_dir.join("rsplayer").join("rsplayer.pid") } /// Check if a process with the given PID is running fn is_process_running(pid: u32) -> bool { unsafe { // send signal 0 to check if process exists libc::kill(pid as i32, 0) == 0 } } /// Check if daemon is running by reading PID file fn check_running_daemon(pid_path: &std::path::Path) -> Result> { if !pid_path.exists() { return Err("PID file not found".into()); } let contents = std::fs::read_to_string(pid_path)?; let pid = contents.trim().parse::()?; if is_process_running(pid) { Ok(pid) } else { // Stale PID file, clean it up std::fs::remove_file(pid_path).ok(); Err("Stale PID file, daemon not running".into()) } } /// Clean up daemon resources (socket and PID file) fn cleanup_daemon(socket_path: &std::path::Path, pid_path: &std::path::Path) { std::fs::remove_file(socket_path).ok(); std::fs::remove_file(pid_path).ok(); }