use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::buffer;
use crate::event::{Arch, Libc, Os};
pub const ROTATION_DAYS: i64 = 90;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstallId {
pub schema_version: u32,
pub install_id: String,
pub rotated_at: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TelemetryState {
#[serde(default)]
pub schema_version: u32,
#[serde(default)]
pub last_version: Option<String>,
#[serde(default)]
pub last_flush: Option<String>,
}
pub fn read_or_create_install_id(root: &Path) -> Result<InstallId> {
buffer::try_with_lock(root, || {
if buffer::tombstone_present(root) {
anyhow::bail!("telemetry is disabled");
}
let path = buffer::install_id_path(root);
let existing = std::fs::read_to_string(&path)
.ok()
.and_then(|body| serde_json::from_str::<InstallId>(&body).ok())
.filter(|record| uuid::Uuid::parse_str(record.install_id.trim()).is_ok())
.filter(|record| !is_expired(&record.rotated_at));
if let Some(record) = existing {
return Ok(record);
}
let record = InstallId {
schema_version: 1,
install_id: uuid::Uuid::new_v4().to_string(),
rotated_at: now_rfc3339(),
};
codewhale_config::persistence::atomic_write_json(&path, &record)
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(record)
})?
.ok_or_else(|| anyhow::anyhow!("telemetry privacy lock is held"))
}
fn is_expired(rotated_at: &str) -> bool {
let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(rotated_at) else {
return true;
};
let age = chrono::Utc::now().signed_duration_since(parsed.with_timezone(&chrono::Utc));
age.num_days() >= ROTATION_DAYS
}
#[must_use]
pub fn read_state(root: &Path) -> TelemetryState {
std::fs::read_to_string(buffer::state_path(root))
.ok()
.and_then(|body| serde_json::from_str::<TelemetryState>(&body).ok())
.unwrap_or_default()
}
pub fn write_state(root: &Path, state: &TelemetryState) -> Result<()> {
buffer::try_with_lock(root, || {
if buffer::tombstone_present(root) {
anyhow::bail!("telemetry is disabled");
}
let path = buffer::state_path(root);
codewhale_config::persistence::atomic_write_json(&path, state)
.with_context(|| format!("failed to write {}", path.display()))
})?
.ok_or_else(|| anyhow::anyhow!("telemetry privacy lock is held"))
}
#[must_use]
pub fn now_rfc3339() -> String {
chrono::Utc::now()
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
.to_string()
}
#[must_use]
pub fn release_build_sha() -> Option<String> {
option_env!("CODEWHALE_RELEASE_BUILD_SHA").and_then(short_hex_sha)
}
#[must_use]
pub fn short_hex_sha(value: &str) -> Option<String> {
let trimmed = value.trim().to_ascii_lowercase();
if trimmed.len() < 12 || !trimmed.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
Some(trimmed.chars().take(12).collect())
}
#[must_use]
pub fn current_os() -> Os {
match std::env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::Macos,
"windows" => Os::Windows,
"freebsd" => Os::Freebsd,
"android" => Os::Android,
_ => Os::Other,
}
}
#[must_use]
pub fn current_arch() -> Arch {
match std::env::consts::ARCH {
"x86_64" => Arch::X86_64,
"aarch64" => Arch::Aarch64,
_ => Arch::Other,
}
}
#[must_use]
pub fn current_libc() -> Libc {
if cfg!(target_env = "gnu") {
Libc::Gnu
} else if cfg!(target_env = "musl") {
Libc::Musl
} else {
Libc::None
}
}
#[must_use]
pub fn current_tty() -> bool {
use std::io::IsTerminal as _;
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
}
#[must_use]
pub fn reduce_panic_site(file: &str, line: u32, column: u32) -> String {
let candidate = format!("{}:{line}:{column}", file.replace('\\', "/"));
if crate::event::is_reduced_panic_site(&candidate) {
candidate
} else {
"<dep>".to_string()
}
}