e061b11974
ci / test (push) Waiting to run
ci / fuzz-smoke (push) Waiting to run
ci / windows-client (push) Waiting to run
ci / package-release (linux-x86_64, ubuntu-latest) (push) Waiting to run
ci / package-release (macos-aarch64, macos-14) (push) Waiting to run
ci / package-release (macos-x86_64, macos-13) (push) Waiting to run
ci / package-release (windows-x86_64, windows-latest) (push) Waiting to run
ci / publish-gitea-release (push) Blocked by required conditions
ci / remote-bench (push) Waiting to run
224 lines
6.2 KiB
Rust
224 lines
6.2 KiB
Rust
use std::fs::{File, OpenOptions};
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{Mutex, OnceLock};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
static TRACE_FILE: OnceLock<Option<Mutex<File>>> = OnceLock::new();
|
|
static HEALTH_FILE: OnceLock<Option<Mutex<File>>> = OnceLock::new();
|
|
static TRACE_BYTES: OnceLock<bool> = OnceLock::new();
|
|
const HEALTH_LOG_MAX_BYTES: u64 = 1024 * 1024;
|
|
|
|
pub fn enabled() -> bool {
|
|
TRACE_FILE.get_or_init(open_trace_file).is_some()
|
|
}
|
|
|
|
pub fn bytes_enabled() -> bool {
|
|
*TRACE_BYTES.get_or_init(|| truthy_env("DOSH_TRACE_BYTES"))
|
|
}
|
|
|
|
pub fn event(name: &str, fields: &[(&str, String)]) {
|
|
let Some(file) = TRACE_FILE.get_or_init(open_trace_file) else {
|
|
return;
|
|
};
|
|
write_event(file, name, fields);
|
|
}
|
|
|
|
pub fn health_event(name: &str, fields: &[(&str, String)]) {
|
|
let Some(file) = HEALTH_FILE.get_or_init(open_health_file) else {
|
|
return;
|
|
};
|
|
write_event(file, name, fields);
|
|
}
|
|
|
|
pub fn default_health_log_path() -> PathBuf {
|
|
trace_root().join("health.log")
|
|
}
|
|
|
|
fn write_event(file: &Mutex<File>, name: &str, fields: &[(&str, String)]) {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|duration| duration.as_millis())
|
|
.unwrap_or(0);
|
|
let mut line = format!(
|
|
"ts_ms={} pid={} event={}",
|
|
now,
|
|
std::process::id(),
|
|
shell_escape(name)
|
|
);
|
|
for (key, value) in fields {
|
|
line.push(' ');
|
|
line.push_str(key);
|
|
line.push('=');
|
|
line.push_str(&shell_escape(value));
|
|
}
|
|
line.push('\n');
|
|
if let Ok(mut file) = file.lock() {
|
|
let _ = file.write_all(line.as_bytes());
|
|
let _ = file.flush();
|
|
}
|
|
}
|
|
|
|
pub fn bytes_summary(bytes: &[u8]) -> String {
|
|
let mut parts = vec![
|
|
format!("len={}", bytes.len()),
|
|
format!(
|
|
"esc={}",
|
|
contains_byte(bytes, 0x1b) || contains_byte(bytes, 0x9b)
|
|
),
|
|
format!("focus={}", has_focus_report(bytes)),
|
|
format!("mouseish={}", looks_mouseish(bytes)),
|
|
format!("printable={}", printable_count(bytes)),
|
|
];
|
|
if bytes_enabled() {
|
|
parts.push(format!("hex={}", hex_prefix(bytes, 160)));
|
|
}
|
|
parts.join(",")
|
|
}
|
|
|
|
fn open_trace_file() -> Option<Mutex<File>> {
|
|
let raw = std::env::var_os("DOSH_TRACE")?;
|
|
let normalized = raw.to_string_lossy().to_ascii_lowercase();
|
|
if normalized.is_empty() || matches!(normalized.as_str(), "0" | "false" | "off") {
|
|
return None;
|
|
}
|
|
let path = if matches!(normalized.as_str(), "1" | "true" | "on") {
|
|
default_trace_path()
|
|
} else {
|
|
PathBuf::from(raw)
|
|
};
|
|
if let Some(parent) = path.parent() {
|
|
let _ = std::fs::create_dir_all(parent);
|
|
}
|
|
OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&path)
|
|
.ok()
|
|
.map(Mutex::new)
|
|
}
|
|
|
|
fn open_health_file() -> Option<Mutex<File>> {
|
|
let path = health_log_path_from_env()?;
|
|
rotate_health_log_if_needed(&path);
|
|
if let Some(parent) = path.parent() {
|
|
let _ = std::fs::create_dir_all(parent);
|
|
}
|
|
OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&path)
|
|
.ok()
|
|
.map(Mutex::new)
|
|
}
|
|
|
|
fn health_log_path_from_env() -> Option<PathBuf> {
|
|
match std::env::var_os("DOSH_HEALTH_LOG") {
|
|
Some(raw) => {
|
|
let normalized = raw.to_string_lossy().to_ascii_lowercase();
|
|
if normalized.is_empty() || matches!(normalized.as_str(), "0" | "false" | "off") {
|
|
None
|
|
} else if matches!(normalized.as_str(), "1" | "true" | "on") {
|
|
Some(default_health_log_path())
|
|
} else {
|
|
Some(PathBuf::from(raw))
|
|
}
|
|
}
|
|
None => Some(default_health_log_path()),
|
|
}
|
|
}
|
|
|
|
fn rotate_health_log_if_needed(path: &Path) {
|
|
let Ok(metadata) = std::fs::metadata(path) else {
|
|
return;
|
|
};
|
|
if metadata.len() <= HEALTH_LOG_MAX_BYTES {
|
|
return;
|
|
}
|
|
let rotated = path.with_extension("log.1");
|
|
let _ = std::fs::remove_file(&rotated);
|
|
let _ = std::fs::rename(path, rotated);
|
|
}
|
|
|
|
fn default_trace_path() -> PathBuf {
|
|
let exe = std::env::current_exe()
|
|
.ok()
|
|
.and_then(|path| {
|
|
path.file_stem()
|
|
.map(|stem| stem.to_string_lossy().to_string())
|
|
})
|
|
.unwrap_or_else(|| "dosh".to_string());
|
|
trace_root().join(format!("{}-{}.log", exe, std::process::id()))
|
|
}
|
|
|
|
fn trace_root() -> PathBuf {
|
|
dirs::cache_dir()
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join("dosh")
|
|
.join("trace")
|
|
}
|
|
|
|
fn truthy_env(name: &str) -> bool {
|
|
std::env::var_os(name).is_some_and(|value| {
|
|
let normalized = value.to_string_lossy().to_ascii_lowercase();
|
|
!normalized.is_empty() && !matches!(normalized.as_str(), "0" | "false" | "off")
|
|
})
|
|
}
|
|
|
|
fn shell_escape(value: &str) -> String {
|
|
if value.bytes().all(|byte| {
|
|
byte.is_ascii_alphanumeric()
|
|
|| matches!(byte, b'.' | b'-' | b'_' | b':' | b'/' | b',' | b'=')
|
|
}) {
|
|
return value.to_string();
|
|
}
|
|
let mut out = String::from("'");
|
|
for ch in value.chars() {
|
|
if ch == '\'' {
|
|
out.push_str("'\\''");
|
|
} else {
|
|
out.push(ch);
|
|
}
|
|
}
|
|
out.push('\'');
|
|
out
|
|
}
|
|
|
|
fn contains_byte(bytes: &[u8], needle: u8) -> bool {
|
|
bytes.contains(&needle)
|
|
}
|
|
|
|
fn has_focus_report(bytes: &[u8]) -> bool {
|
|
bytes
|
|
.windows(3)
|
|
.any(|window| matches!(window, b"\x1b[I" | b"\x1b[O"))
|
|
|| bytes
|
|
.windows(2)
|
|
.any(|window| matches!(window, b"\x9bI" | b"\x9bO"))
|
|
}
|
|
|
|
fn looks_mouseish(bytes: &[u8]) -> bool {
|
|
bytes.windows(3).any(|window| window == b"\x1b[<")
|
|
|| bytes.windows(2).any(|window| window == b"\x9b<")
|
|
|| bytes.iter().filter(|byte| **byte == b';').take(3).count() >= 2
|
|
}
|
|
|
|
fn printable_count(bytes: &[u8]) -> usize {
|
|
bytes
|
|
.iter()
|
|
.filter(|byte| byte.is_ascii_graphic() || **byte == b' ')
|
|
.count()
|
|
}
|
|
|
|
fn hex_prefix(bytes: &[u8], max: usize) -> String {
|
|
let mut out = String::with_capacity(bytes.len().min(max) * 2);
|
|
for byte in bytes.iter().take(max) {
|
|
use std::fmt::Write as _;
|
|
let _ = write!(&mut out, "{byte:02x}");
|
|
}
|
|
if bytes.len() > max {
|
|
out.push_str("...");
|
|
}
|
|
out
|
|
}
|