eoka 0.7.1

Stealth browser automation for Rust. Puppeteer/Playwright alternative with anti-bot bypass.
Documentation
use crate::{Error, Result};
use std::path::{Path, PathBuf};

pub(crate) struct RuntimeDirectory {
    path: Option<PathBuf>,
}

impl RuntimeDirectory {
    pub(crate) fn create(root: Option<&str>) -> Result<Self> {
        let root = root.map(PathBuf::from).unwrap_or_else(std::env::temp_dir);
        if !root.is_absolute() {
            return Err(Error::Launch("runtime_dir must be absolute".into()));
        }
        std::fs::create_dir_all(&root)?;
        let root = std::fs::canonicalize(root)?;
        for _ in 0..100 {
            let path = root.join(format!("eoka-{:016x}", fastrand::u64(..)));
            #[cfg(unix)]
            let created = {
                use std::os::unix::fs::DirBuilderExt;
                std::fs::DirBuilder::new().mode(0o700).create(&path)
            };
            #[cfg(not(unix))]
            let created = std::fs::create_dir(&path);
            match created {
                Ok(()) => return Ok(Self { path: Some(path) }),
                Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
                Err(error) => return Err(error.into()),
            }
        }
        Err(Error::Launch(
            "Cannot allocate a unique browser runtime directory".into(),
        ))
    }

    pub(crate) fn path(&self) -> &Path {
        self.path.as_deref().expect("runtime ownership is active")
    }
    pub(crate) fn cleanup(&mut self) -> std::io::Result<()> {
        if let Some(path) = &self.path {
            remove_dir_if_exists(path).map_err(|error| {
                std::io::Error::new(
                    error.kind(),
                    format!(
                        "Failed to remove owned browser runtime {}: {}",
                        path.display(),
                        error
                    ),
                )
            })?;
            self.path = None;
        }
        Ok(())
    }
}

impl Drop for RuntimeDirectory {
    fn drop(&mut self) {
        if let Err(error) = self.cleanup() {
            tracing::warn!("Failed to remove owned browser runtime: {}", error);
        }
    }
}

fn remove_dir_if_exists(path: &Path) -> std::io::Result<()> {
    for attempt in 0..10 {
        match std::fs::remove_dir_all(path) {
            Ok(()) => return Ok(()),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
            Err(error)
                if matches!(error.raw_os_error(), Some(39) | Some(66) | Some(145))
                    && attempt < 9 =>
            {
                std::thread::sleep(std::time::Duration::from_millis(50));
            }
            Err(error) => return Err(error),
        }
    }
    Ok(())
}