pryr 0.4.1

A blazing-fast, uncompromising Islamic prayer time daemon and screen locker for Linux and Windows
Documentation
#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]
use pryr::{
    config::Config,
    daemon::{DaemonSnapShot, DaemonState},
    ipc::{IpcListener, IpcRequest, IpcResponse},
    prayers::{ActionableEvent, PrayerManager},
    system::{self, get_config_path},
};
use salah::Utc;
use std::time::Duration;
use tokio::{
    fs::create_dir_all,
    io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
    sync::{mpsc, watch},
};

const LOCKDOWN_POLL_DURATION: Duration = Duration::from_secs(10);

#[tokio::main(flavor = "current_thread")]
async fn main() {
    if let Some(path) = get_config_path() {
        let config = if path.exists() {
            match Config::from_file(&path) {
                Ok(config) => config,
                Err(e) => {
                    eprintln!(
                        "[ERROR] Couldn't parse Configuration File at {:?}: {}",
                        path, e
                    );
                    return;
                }
            }
        } else {
            let config = Config::default();
            let parent = if let Some(parent) = path.parent() {
                parent
            } else {
                eprintln!("[ERROR] Path should end in config.toml");
                return;
            };

            if let Err(e) = create_dir_all(parent).await {
                eprintln!("[ERROR] Could not create config directory: {}", e);
                return;
            }

            if let Err(e) = config.save(&path) {
                eprintln!("[ERROR] Could not save default config: {}", e);
                return;
            }
            config
        };

        println!("[INFO] Starting daemon...");
        let (watch_tx, watch_rx) = watch::channel(DaemonSnapShot::default());
        let (mpsc_tx, mpsc_rx) = mpsc::channel::<IpcRequest>(10);

        let h1 = tokio::spawn(daemon_loop(config, watch_tx, mpsc_rx));
        let h2 = tokio::spawn(ipc_server_loop(watch_rx, mpsc_tx));

        match h1.await {
            Ok(Ok(_)) => (),
            Ok(Err(e)) => eprintln!("[ERROR] daemon_loop returned an error: {}", e),
            Err(e) => eprintln!(
                "[ERROR] Main daemon task panicked. This is likely due to an invalid configuration that prevents prayer time calculation. {e}",
            ),
        }
        match h2.await {
            Ok(Ok(_)) => (),
            Ok(Err(e)) => eprintln!("[ERROR] ipc_server_loop returned an error: {}", e),
            Err(e) => eprintln!("[ERROR] ipc_server_loop panicked: {}", e),
        }
    } else {
        eprintln!("[ERROR] Could not get config path");
    }
}

async fn ipc_server_loop(
    watch_rx: watch::Receiver<DaemonSnapShot>,
    mpsc_tx: mpsc::Sender<IpcRequest>,
) -> anyhow::Result<()> {
    let listener = IpcListener::bind().await?;

    loop {
        let stream = listener.accept().await?;
        let watch_rx_clone = watch_rx.clone();
        let mpsc_tx_clone = mpsc_tx.clone();

        tokio::spawn(async move {
            let (read_half, mut write_half) = tokio::io::split(stream);
            let mut buf_reader = BufReader::new(read_half);
            let mut s = String::new();
            buf_reader.read_line(&mut s).await?;

            let response: IpcResponse = match serde_json::from_str::<IpcRequest>(&s) {
                Ok(request) => match request {
                    IpcRequest::GetStatus => {
                        let state = watch_rx_clone.borrow();
                        IpcResponse::CurrentState(state.current_state)
                    }
                    IpcRequest::GetTodaySchedule => {
                        let schedule = watch_rx_clone.borrow().daily_schedule.clone();
                        IpcResponse::DailySchedule(schedule)
                    }
                    IpcRequest::ReloadConfig => {
                        match mpsc_tx_clone.send(IpcRequest::ReloadConfig).await {
                            Ok(_) => IpcResponse::Success,
                            Err(e) => IpcResponse::Error(e.to_string()),
                        }
                    }
                },
                Err(_) => IpcResponse::Error("Invalid Command".to_string()),
            };

            let response_string = serde_json::to_string(&response)?;
            write_half.write_all(response_string.as_bytes()).await?;
            write_half.flush().await?;

            anyhow::Ok(())
        });
    }
}

async fn daemon_loop(
    mut config: Config,
    watch_tx: watch::Sender<DaemonSnapShot>,
    mut mpsc_rx: mpsc::Receiver<IpcRequest>,
) -> anyhow::Result<()> {
    let mut prayer_manager = PrayerManager::new(&config);
    let mut state = DaemonState::Calculating;

    loop {
        state = match state {
            DaemonState::Calculating => {
                let event = prayer_manager.get_next_actionable_event(Utc::now());

                let next_event = match event {
                    ActionableEvent::WaitForPrayer(name, time) => {
                        DaemonState::WaitingForPrayer(name, time)
                    }
                    ActionableEvent::WaitForIqamah(name, time) => {
                        DaemonState::IqamahWarning(name, time)
                    }
                    ActionableEvent::Skip => panic!("Shouldn't happen"),
                };

                watch_tx.send(DaemonSnapShot::new(
                    next_event,
                    &mut prayer_manager,
                    config.iqamah_offset,
                ))?;

                next_event
            }
            DaemonState::WaitingForPrayer(prayer, time) => {
                println!("[INFO] Next prayer is {prayer:?} at {time}");
                println!("[INFO] Sleeping until prayer time");

                tokio::select! {
                    biased;
                    Some(IpcRequest::ReloadConfig) = mpsc_rx.recv() => {
                        println!("[INFO] Received reload config request. Reloading...");
                        (prayer_manager, config) = system::reload();
                        DaemonState::Calculating
                    },
                    _ = system::sleep_until_datetime(time) => {
                        println!("[INFO] Woke up for prayer: {prayer:?}");

                        system::notify(
                            &format!("Prayer {prayer} has started"),
                            &format!(
                                "Iqamah in {} minutes",
                                prayer_manager
                                    .time_left_for_iqamah(prayer, time)
                                    .unwrap()
                                    .num_minutes()
                            ),
                        )
                        .await?;

                        let iqamah_time = prayer_manager.get_iqamah_time(prayer, time).unwrap();
                        let next_event = DaemonState::IqamahWarning(prayer, iqamah_time);

                        watch_tx.send(DaemonSnapShot::new(
                            next_event,
                            &mut prayer_manager,
                            config.iqamah_offset,
                        ))?;

                        next_event
                    },
                }
            }
            DaemonState::IqamahWarning(prayer, iqamah_time) => {
                println!(
                    "[INFO] Sleeping until {} minutes before iqamah",
                    config.lockdown.warning_before_iqamah
                );
                let warning_time =
                    iqamah_time - Duration::from_mins(config.lockdown.warning_before_iqamah.into());

                tokio::select! {
                    biased;
                    Some(IpcRequest::ReloadConfig) = mpsc_rx.recv() => {
                        println!("[INFO] Received reload config request. Reloading...");
                        (prayer_manager, config) = system::reload();
                        DaemonState::Calculating
                    },

                    _ =  system::sleep_until_datetime(warning_time) => {
                        system::notify(
                            &format!("{prayer} Iqamah in {} minutes", config.lockdown.warning_before_iqamah),
                            &format!("Get ready! Lockdown in {} minutes!", config.lockdown.lock_before_iqamah),
                        )
                        .await?;

                        let lockdown_time = iqamah_time - Duration::from_mins(config.lockdown.lock_before_iqamah.into());
                        let next_event = DaemonState::LockdownWarning(prayer, lockdown_time);

                        watch_tx.send(DaemonSnapShot::new(
                            next_event,
                            &mut prayer_manager,
                            config.iqamah_offset,
                        ))?;

                        next_event
                    }
                }
            }
            DaemonState::LockdownWarning(prayer, lockdown_time) => {
                tokio::select! {
                    biased;
                    Some(IpcRequest::ReloadConfig) = mpsc_rx.recv() => {
                        println!("[INFO] Received reload config request. Reloading...");
                        (prayer_manager, config) = system::reload();
                        DaemonState::Calculating
                    },

                    _ = system::sleep_until_datetime(lockdown_time) => {

                        system::notify(
                            &format!("{prayer} Iqamah in {} minutes", config.lockdown.lock_before_iqamah),
                            "Get ready! Lockdown in 30 seconds!",
                        )
                        .await?;

                        tokio::time::sleep(Duration::from_secs(30)).await;
                        println!("[INFO] Initiating lockdown for prayer: {prayer:?}");

                        let unlock_time =
                            lockdown_time
                            + Duration::from_mins(config.lockdown.lock_before_iqamah.into())
                            + Duration::from_mins(config.lockdown.unlock_after_iqamah.into());
                        let next_event = DaemonState::Lockdown(unlock_time);

                        watch_tx.send(DaemonSnapShot::new(
                            next_event,
                            &mut prayer_manager,
                            config.iqamah_offset,
                        ))?;

                        next_event
                    }

                }
            }
            DaemonState::Lockdown(unlock_time) => {
                while Utc::now() < unlock_time {
                    if config.options.lock_screen {
                        system::lock_screen().await?;
                    } else {
                        system::notify(
                            "Iqamah has started!!",
                            "Leave your PC and go pray already!",
                        )
                        .await?;
                    }

                    tokio::time::sleep(LOCKDOWN_POLL_DURATION).await;
                }

                println!("[INFO] Lockdown finished");

                let next_event = DaemonState::Calculating;

                watch_tx.send(DaemonSnapShot::new(
                    next_event,
                    &mut prayer_manager,
                    config.iqamah_offset,
                ))?;

                next_event
            }
        }
    }
}