calepin 0.0.17

A Rust CLI for preprocessing Typst documents with executable code chunks
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{anyhow, Context, Result};

use crate::typst::io::write_if_changed;

const WATCH_PID_FILENAME: &str = "watch.pid";

pub(super) fn stop_watch_from_pid_file(path: &Path) -> Result<bool> {
    let pid = read_watch_pid(path)?;
    let stopped = if is_process_running(pid) {
        terminate_process(pid)?;
        true
    } else {
        false
    };
    let _ = fs::remove_file(path);
    Ok(stopped)
}

fn read_watch_pid(path: &Path) -> Result<u32> {
    let contents = fs::read_to_string(path)
        .with_context(|| format!("failed to read watch pid file {}", path.display()))?;
    contents
        .trim()
        .parse::<u32>()
        .with_context(|| format!("invalid watch pid in {}", path.display()))
}

pub(super) fn write_watch_pid_file(path: &Path, pid: u32) -> Result<()> {
    write_if_changed(path, pid.to_string())
        .with_context(|| format!("failed to write watch pid file {}", path.display()))
}

pub(super) fn remove_watch_pid_file(path: &Path) -> Result<()> {
    fs::remove_file(path).context("failed to remove watch pid file")?;
    Ok(())
}

pub(super) fn watch_pid_file_path(results_path: &Path) -> PathBuf {
    let parent = results_path
        .parent()
        .expect("results path should have a parent directory");
    parent.join(WATCH_PID_FILENAME)
}

pub(super) fn collect_watch_pid_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
    let entries = match fs::read_dir(dir) {
        Ok(entries) => entries,
        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
        Err(error) => return Err(error.into()),
    };
    for entry in entries {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let entry_path = entry.path();
        if file_type.is_dir() {
            collect_watch_pid_files(&entry_path, out)?;
        } else if file_type.is_file() && entry.file_name() == WATCH_PID_FILENAME {
            out.push(entry_path);
        }
    }
    Ok(())
}

#[cfg(unix)]
fn is_process_running(pid: u32) -> bool {
    Command::new("kill")
        .arg("-0")
        .arg(pid.to_string())
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

#[cfg(unix)]
fn terminate_process(pid: u32) -> Result<()> {
    let status = Command::new("kill")
        .arg("-TERM")
        .arg(pid.to_string())
        .status()
        .with_context(|| format!("failed to signal watch process {pid}"))?;
    if !status.success() {
        return Err(anyhow!("failed to signal watch process {pid}"));
    }
    Ok(())
}

#[cfg(windows)]
fn is_process_running(_pid: u32) -> bool {
    true
}

#[cfg(windows)]
fn terminate_process(pid: u32) -> Result<()> {
    let status = Command::new("taskkill")
        .arg("/PID")
        .arg(pid.to_string())
        .arg("/F")
        .status()
        .with_context(|| format!("failed to signal watch process {pid}"))?;
    if !status.success() {
        return Err(anyhow!("failed to signal watch process {pid}"));
    }
    Ok(())
}