activity-tracker 1.13.0

Captura atividades do sistema e resume com Ollama
use crate::{collector, config};
use anyhow::{Context, Result};
use tokio::signal;
use tokio::time::{self, Duration};

pub fn pid_file() -> std::path::PathBuf {
    crate::config::data_dir().join("daemon.pid")
}

pub async fn run(interval_min: u64, foreground: bool) -> Result<()> {
    if !foreground {
        spawn_background(interval_min)?;
        return Ok(());
    }
    run_foreground(interval_min).await
}

fn spawn_background(interval_min: u64) -> Result<()> {
    use std::os::unix::process::CommandExt;

    let exe = std::env::current_exe().context("não foi possível obter caminho do binário")?;

    let child = std::process::Command::new(exe)
        .args([
            "start",
            "--foreground",
            "--interval",
            &interval_min.to_string(),
        ])
        .stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .process_group(0)
        .spawn()
        .context("falha ao iniciar daemon em background")?;

    println!("Daemon iniciado (PID {})", child.id());
    println!("  Logs:  {}", config::log_dir().display());
    println!("  Parar: activity-tracker stop");
    Ok(())
}

async fn run_foreground(interval_min: u64) -> Result<()> {
    let log_dir = config::log_dir();

    let pid_path = pid_file();
    if let Some(parent) = pid_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(&pid_path, std::process::id().to_string())?;
    let _guard = PidGuard(pid_path);

    let mut tick = time::interval(Duration::from_secs(interval_min * 60));

    do_collect(log_dir.clone()).await;

    loop {
        tokio::select! {
            _ = tick.tick() => { do_collect(log_dir.clone()).await; }
            _ = signal::ctrl_c() => { break; }
        }
    }

    Ok(())
}

struct PidGuard(std::path::PathBuf);
impl Drop for PidGuard {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.0);
    }
}

pub fn stop() -> Result<()> {
    let pid_path = pid_file();

    if !pid_path.exists() {
        println!("Daemon não está rodando.");
        return Ok(());
    }

    let pid_str = std::fs::read_to_string(&pid_path)?;
    let pid: u32 = pid_str.trim().parse().context("PID inválido no arquivo")?;

    let ok = std::process::Command::new("kill")
        .arg(pid.to_string())
        .status()
        .map(|s| s.success())
        .unwrap_or(false);

    if ok {
        println!("Daemon (PID {pid}) parado.");
    } else {
        println!("Não foi possível parar PID {pid} — pode já ter encerrado.");
    }
    let _ = std::fs::remove_file(&pid_path);

    Ok(())
}

pub fn status() -> Result<()> {
    let pid_path = pid_file();

    if !pid_path.exists() {
        println!("Daemon: parado");
        return Ok(());
    }

    let pid_str = std::fs::read_to_string(&pid_path)?;
    let pid: u32 = pid_str.trim().parse().unwrap_or(0);

    if std::path::Path::new(&format!("/proc/{pid}")).exists() {
        println!("Daemon: rodando (PID {pid})");
        println!("  Logs: {}", config::log_dir().display());
    } else {
        println!("Daemon: parado (PID file obsoleto removido)");
        let _ = std::fs::remove_file(&pid_path);
    }

    Ok(())
}

async fn do_collect(log_dir: std::path::PathBuf) {
    let now = chrono::Local::now().format("%H:%M:%S").to_string();
    let (blocked, ignored_git_paths) = config::Config::load()
        .map(|c| (c.blocked_patterns, c.ignored_git_paths))
        .unwrap_or_default();
    let result = tokio::task::spawn_blocking(move || {
        collector::collect_all(&log_dir, &blocked, &ignored_git_paths)
    })
    .await;
    match result {
        Ok(Ok(n)) => eprintln!("[{now}] {n} entradas coletadas"),
        Ok(Err(e)) => eprintln!("[{now}] Erro na coleta: {e}"),
        Err(e) => eprintln!("[{now}] Erro interno: {e}"),
    }
}