use anyhow::Result;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Once;
use crate::session;
static CLEANUP_ONCE: Once = Once::new();
pub fn session_log_path() -> std::path::PathBuf {
if let Ok(path) = env::var("SIVTR_SESSION_LOG") {
let path = PathBuf::from(path);
cleanup_stale_sessions_once(&path);
return path;
}
if let Ok(path) = env::var("SIFT_SESSION_LOG") {
let path = PathBuf::from(path);
cleanup_stale_sessions_once(&path);
return path;
}
let base_dir = dirs::config_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
let current = base_dir.join("sivtr").join("session.log");
if current.exists() {
return current;
}
let legacy = base_dir.join("sift").join("session.log");
if legacy.exists() {
return legacy;
}
current
}
fn cleanup_stale_sessions_once(current_session_path: &Path) {
let current_session_path = current_session_path.to_path_buf();
CLEANUP_ONCE.call_once(|| {
let _ = cleanup_stale_sessions(¤t_session_path);
});
}
fn cleanup_stale_sessions(current_session_path: &Path) -> Result<()> {
let Some(parent) = current_session_path.parent() else {
return Ok(());
};
if !parent.exists() {
return Ok(());
}
let current_pid = session_pid_from_path(current_session_path);
for entry in std::fs::read_dir(parent)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(pid) = session_pid_from_path(&path) else {
continue;
};
if Some(pid) == current_pid || process_is_alive(pid) {
continue;
}
let _ = std::fs::remove_file(path);
}
Ok(())
}
fn session_pid_from_path(path: &Path) -> Option<u32> {
let name = path.file_name()?.to_str()?;
let suffix = if let Some(pid) = name
.strip_prefix("session_")
.and_then(|rest| rest.strip_suffix(".log"))
{
pid
} else if let Some(pid) = name
.strip_prefix("session_")
.and_then(|rest| rest.strip_suffix(".state"))
{
pid
} else {
return None;
};
suffix.parse::<u32>().ok()
}
#[cfg(windows)]
fn process_is_alive(pid: u32) -> bool {
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION;
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return false;
}
let _ = CloseHandle(handle);
true
}
}
#[cfg(unix)]
fn process_is_alive(pid: u32) -> bool {
let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
if result == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
pub fn flush_state_path() -> std::path::PathBuf {
session_log_path().with_extension("state")
}
pub fn read_session_log() -> Result<Option<String>> {
let log = session_log_path();
if log.exists() {
let entries = session::load_entries(&log)?;
if !entries.is_empty() {
return Ok(Some(session::render_entries_ansi(&entries)));
}
}
Ok(None)
}
#[cfg(windows)]
pub struct ConsoleSnapshot {
pub content: String,
pub width: usize,
}
#[cfg(windows)]
pub fn capture_console_buffer() -> Result<ConsoleSnapshot> {
capture_windows_console()
}
#[cfg(windows)]
fn capture_windows_console() -> Result<ConsoleSnapshot> {
use std::ptr;
use winapi::um::fileapi::{CreateFileW, OPEN_EXISTING};
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
use winapi::um::processenv::GetStdHandle;
use winapi::um::winbase::STD_OUTPUT_HANDLE;
use winapi::um::wincon::{
GetConsoleScreenBufferInfo, ReadConsoleOutputAttribute, ReadConsoleOutputCharacterW,
CONSOLE_SCREEN_BUFFER_INFO, COORD,
};
use winapi::um::winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE};
unsafe {
let conout: Vec<u16> = "CONOUT$\0".encode_utf16().collect();
let conout_handle = CreateFileW(
conout.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
);
let (handle, should_close) =
if !conout_handle.is_null() && conout_handle != INVALID_HANDLE_VALUE {
(conout_handle, true)
} else {
let std_handle = GetStdHandle(STD_OUTPUT_HANDLE);
(std_handle, false)
};
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
anyhow::bail!("Failed to get console handle");
}
let mut info: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed();
if GetConsoleScreenBufferInfo(handle, &mut info) == 0 {
if should_close {
CloseHandle(handle);
}
anyhow::bail!("Failed to get console buffer info");
}
let width = info.dwSize.X as usize;
if width == 0 {
return Ok(ConsoleSnapshot {
content: String::new(),
width: 0,
});
}
let cursor_row = info.dwCursorPosition.Y as usize;
let content_rows = (cursor_row + 1).min(info.dwSize.Y as usize);
let default_attr = info.wAttributes;
let mut result = String::with_capacity(content_rows * (width + 20));
let mut char_buf = vec![0u16; width];
let mut attr_buf = vec![0u16; width];
for row in 0..content_rows {
let mut chars_read: u32 = 0;
let mut attrs_read: u32 = 0;
let coord = COORD {
X: 0,
Y: row as i16,
};
ReadConsoleOutputCharacterW(
handle,
char_buf.as_mut_ptr(),
width as u32,
coord,
&mut chars_read,
);
ReadConsoleOutputAttribute(
handle,
attr_buf.as_mut_ptr(),
width as u32,
coord,
&mut attrs_read,
);
let line_str = String::from_utf16_lossy(&char_buf[..chars_read as usize]);
let trimmed = line_str.trim_end();
if trimmed.is_empty() {
result.push('\n');
continue;
}
let trimmed_len = trimmed.len();
let mut prev_attr = default_attr;
let mut byte_idx: usize = 0;
let mut cell_idx: usize = 0;
for ch in line_str.chars() {
if byte_idx >= trimmed_len {
break;
}
let attr = if cell_idx < attrs_read as usize {
attr_buf[cell_idx]
} else {
default_attr
};
if attr != prev_attr || cell_idx == 0 {
if attr != default_attr {
result.push_str(&win_attr_to_ansi(attr));
} else if prev_attr != default_attr {
result.push_str("\x1b[0m");
}
prev_attr = attr;
}
result.push(ch);
byte_idx += ch.len_utf8();
if unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) > 1 {
cell_idx += 2;
} else {
cell_idx += 1;
}
}
if prev_attr != default_attr {
result.push_str("\x1b[0m");
}
result.push('\n');
}
if should_close {
CloseHandle(handle);
}
let trimmed = result.trim_end_matches('\n').to_string();
Ok(ConsoleSnapshot {
content: trimmed,
width,
})
}
}
#[cfg(windows)]
fn win_attr_to_ansi(attr: u16) -> String {
const WIN_TO_ANSI: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7];
let fg_bits = (attr & 0x07) as usize;
let fg_intense = (attr & 0x08) != 0;
let bg_bits = ((attr >> 4) & 0x07) as usize;
let bg_intense = (attr & 0x80) != 0;
let fg_ansi = WIN_TO_ANSI[fg_bits];
let bg_ansi = WIN_TO_ANSI[bg_bits];
let mut codes = Vec::new();
if fg_intense {
codes.push(format!("{}", 90 + fg_ansi)); } else {
codes.push(format!("{}", 30 + fg_ansi)); }
if bg_bits != 0 || bg_intense {
if bg_intense {
codes.push(format!("{}", 100 + bg_ansi));
} else {
codes.push(format!("{}", 40 + bg_ansi));
}
}
format!("\x1b[{}m", codes.join(";"))
}
#[cfg(test)]
mod tests {
use super::session_pid_from_path;
use std::path::Path;
#[test]
fn extracts_pid_from_session_artifacts() {
assert_eq!(session_pid_from_path(Path::new("session_42.log")), Some(42));
assert_eq!(
session_pid_from_path(Path::new("session_42.state")),
Some(42)
);
assert_eq!(session_pid_from_path(Path::new("session.log")), None);
assert_eq!(session_pid_from_path(Path::new("history.db")), None);
}
}