Files
endorphin-tessera-gui/src/shell/activity.rs

100 lines
3.3 KiB
Rust

//! 有界 HTTP 活动记录。保存请求/响应的可显示预览供 Network 工具读取,不负责执行请求。
//! 正文预览和允许显示的头部受限制,避免活动面板成为无限日志或暴露不必要数据的通道。
//!
//! Bounded observation of this process's local HTTP API, never external traffic.
use std::{
collections::VecDeque,
sync::{Arc, Mutex},
time::{Instant, SystemTime, UNIX_EPOCH},
};
pub type Activity = Arc<Mutex<ActivityLog>>;
pub const BODY_PREVIEW: usize = 32 * 1024;
const MAX_RECORDS: usize = 80;
#[derive(Clone, Debug)]
/// 一次 HTTP 请求/响应的有界观察记录,供 Network 工具查看。
pub struct Exchange {
pub id: u64,
pub method: String,
pub path: String,
pub origin: String,
pub timestamp: String,
pub request: String,
pub response: String,
pub status: Option<u16>,
pub elapsed_ms: u128,
pub request_headers: String,
pub response_headers: String,
started: Instant,
}
#[derive(Debug, Default)]
/// 按容量维护近期 HTTP 活动与请求身份,防止无限日志积累。
pub struct ActivityLog {
pub records: VecDeque<Exchange>,
next_id: u64,
}
impl ActivityLog {
/// 为请求分配活动 ID 并登记其方法、来源和受限头部。
pub fn begin(&mut self, method: String, path: String, origin: String, headers: String) -> u64 {
self.next_id += 1;
let id = self.next_id;
let seconds = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
% 86400;
self.records.push_back(Exchange {
id,
method,
path: preview(&path),
origin,
timestamp: format!(
"{:02}:{:02}:{:02} UTC",
seconds / 3600,
seconds / 60 % 60,
seconds % 60
),
request: String::new(),
response: String::new(),
status: None,
elapsed_ms: 0,
request_headers: preview(&headers),
response_headers: String::new(),
started: Instant::now(),
});
while self.records.len() > MAX_RECORDS {
self.records.pop_front();
}
id
}
/// 更新请求正文预览,内容长度仍由活动记录策略限制。
pub fn body(&mut self, id: u64, body: &str) {
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
record.request = preview(body);
}
}
/// 完成本模块当前阶段的结果整理,把后续工作交回调用方。
pub fn finish(&mut self, id: u64, status: u16, body: &str, headers: String) {
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
record.status = Some(status);
record.response = preview(body);
record.elapsed_ms = record.started.elapsed().as_millis();
record.response_headers = preview(&headers);
}
}
}
/// 截取可显示的受限文本预览,避免日志无限保留完整大正文。
pub fn preview(value: &str) -> String {
if value.len() <= BODY_PREVIEW {
return value.to_owned();
}
let mut end = BODY_PREVIEW;
while !value.is_char_boundary(end) {
end -= 1;
}
format!("{}\n[preview truncated at 32 KiB]", &value[..end])
}