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

110 lines
4.5 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},
};
// HTTP worker 与原生 Network 工具共享同一有界日志;持锁读取快照/更新记录,不在这里执行网络请求。
pub type Activity = Arc<Mutex<ActivityLog>>;
// 预览正文按 UTF-8 字节限制;截断标记会额外占用少量字节。
pub const BODY_PREVIEW: usize = 32 * 1024;
// 只保留最近 80 次开始的请求,尚未完成的老请求也可能被淘汰。
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,
// None 表示尚未记录响应;完成后保存 HTTP 状态码,不据此判断业务调用是否成功。
pub status: Option<u16>,
pub elapsed_ms: u128,
pub request_headers: String,
pub response_headers: String,
// 使用单调时钟计算耗时,不受系统时间调整影响;timestamp 则用于显示 UTC 时刻。
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;
// 显示时间取 Unix 秒数的当日余数;系统时间早于纪元时回退到零。这里只显示时间,不保存日期。
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(),
});
// 从队首淘汰最早开始的记录。后续 body/finish 若找不到其 ID 就忽略,不重新插入旧请求。
while self.records.len() > MAX_RECORDS {
self.records.pop_front();
}
id
}
/// 更新请求正文预览,内容长度仍由活动记录策略限制。
// 线性查找在最多 80 项范围内完成;本方法覆盖之前的请求预览,不拼接分块正文。
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);
}
}
/// 为指定请求登记响应状态、正文/头部预览与单调时钟耗时;记录已淘汰时忽略。
// 仅更新仍保留的请求,响应正文和头部都重新截断;耗时从 begin 起计算。
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();
}
// UTF-8 多字节字符不能从中间切开;从字节上限回退到合法边界,确保输出始终是有效字符串。
let mut end = BODY_PREVIEW;
while !value.is_char_boundary(end) {
end -= 1;
}
format!("{}\n[preview truncated at 32 KiB]", &value[..end])
}