use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::Mutex;
use std::time::Instant;
pub const ENV_DEBUG_FILE: &str = "TERMWRIGHT_DEBUG_FILE";
pub const ENV_DEBUG: &str = "TERMWRIGHT_DEBUG";
const DRIVER_SWITCHES: [&str; 8] = ["0", "1", "true", "false", "on", "off", "api", "all"];
const MAX_MESSAGE: usize = 400;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
Diag,
Sem,
Io,
App,
}
impl Category {
fn as_str(self) -> &'static str {
match self {
Self::Diag => "diag",
Self::Sem => "sem",
Self::Io => "io",
Self::App => "app",
}
}
}
pub fn debug_path<F>(lookup: F) -> Option<String>
where
F: Fn(&str) -> Option<String>,
{
if let Some(explicit) = lookup(ENV_DEBUG_FILE) {
let explicit = explicit.trim().to_owned();
if !explicit.is_empty() {
return Some(explicit);
}
}
let raw = lookup(ENV_DEBUG)?.trim().to_owned();
if raw.is_empty() || DRIVER_SWITCHES.contains(&raw.to_ascii_lowercase().as_str()) {
return None;
}
Some(raw)
}
#[derive(Debug)]
pub struct DebugLog {
state: Mutex<State>,
started: Instant,
}
#[derive(Debug)]
struct State {
file: Option<File>,
label: String,
}
impl DebugLog {
#[must_use]
pub fn from_env(adapter: &str) -> Option<Self> {
let path = debug_path(|name| std::env::var(name).ok())?;
Self::open(&path, adapter)
}
#[must_use]
pub fn open(path: &str, adapter: &str) -> Option<Self> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.ok()?;
let log = Self {
state: Mutex::new(State {
file: Some(file),
label: format!("p{}", std::process::id()),
}),
started: Instant::now(),
};
log.line(
Category::Diag,
&format!(
"open adapter={adapter} pid={} platform={}/{} argv0={}",
std::process::id(),
std::env::consts::OS,
std::env::consts::ARCH,
short(&argv0()),
),
);
Some(log)
}
pub fn set_label(&self, label: &str) {
if label.is_empty() {
return;
}
let short = label.chars().take(8).collect::<String>();
if let Ok(mut state) = self.state.lock() {
state.label = short;
}
}
#[must_use]
pub fn label(&self) -> String {
self.state
.lock()
.map(|state| state.label.clone())
.unwrap_or_default()
}
pub fn line(&self, category: Category, message: &str) {
let message = if message.len() > MAX_MESSAGE {
let mut cut = MAX_MESSAGE;
while cut > 0 && !message.is_char_boundary(cut) {
cut -= 1;
}
format!("{}…", &message[..cut])
} else {
message.to_owned()
};
let seconds = self.started.elapsed().as_secs_f64();
let Ok(mut state) = self.state.lock() else {
return;
};
let text = format!(
" tw:{:<4} [{}] {:>7.3}s {message}\n",
category.as_str(),
state.label,
seconds,
);
let failed = match state.file.as_mut() {
Some(file) => file
.write_all(text.as_bytes())
.and_then(|()| file.flush())
.is_err(),
None => false,
};
if failed {
state.file = None;
}
}
pub fn close(&self) {
if let Ok(mut state) = self.state.lock() {
state.file = None;
}
}
}
#[must_use]
pub fn describe_endpoint(endpoint: &str) -> String {
let kind = if endpoint.starts_with(r"\\.\pipe\") || endpoint.starts_with(r"\\?\pipe\") {
"pipe"
} else {
"unix"
};
format!("{kind}:{}", short(endpoint))
}
fn short(value: &str) -> String {
const LIMIT: usize = 60;
if value.chars().count() <= LIMIT {
return value.to_owned();
}
let tail: String = value
.chars()
.skip(value.chars().count() - (LIMIT - 1))
.collect();
format!("…{tail}")
}
fn argv0() -> String {
std::env::args()
.next()
.and_then(|path| {
std::path::Path::new(&path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
})
.unwrap_or_default()
}
pub(crate) fn on_off(enabled: bool) -> &'static str {
if enabled {
"on"
} else {
"off"
}
}
pub(crate) fn error_label(error: &std::io::Error) -> String {
match error.raw_os_error() {
Some(code) => format!("{:?} [errno {code}]: {error}", error.kind()),
None => format!("{:?}: {error}", error.kind()),
}
}
pub(crate) fn join_capabilities(capabilities: &[crate::roles::Capability]) -> String {
capabilities
.iter()
.map(|capability| {
serde_json::to_string(capability)
.unwrap_or_default()
.trim_matches('"')
.to_owned()
})
.collect::<Vec<_>>()
.join(",")
}