//! 有界 HTTP 活动记录。保存请求/响应的可显示预览供 Network 工具读取,不负责执行请求。 //! 本模块限制预览字节数与记录条数;请求头白名单由 runtime/api::ObservedRequest 提前筛选。 //! //! 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>; // 预览正文按 UTF-8 字节限制;截断标记会额外占用少量字节。 pub const BODY_PREVIEW: usize = 32 * 1024; // 只保留最近 80 次开始的请求,尚未完成的老请求也可能被淘汰。 const MAX_RECORDS: usize = 80; #[derive(Clone, Debug)] /// 一次 HTTP 请求/响应的有界观察记录,供 Network 工具查看。 pub struct Exchange { // 由 begin 分配的日志身份,HTTP worker 后续只携带此 ID 更新,不持有队列元素引用。 pub id: u64, // 请求方法由本地 API 观察器传入,按原文本保存;这里不校验或执行 HTTP 方法。 pub method: String, // 请求 URL 的受限预览,不在此解析路由或读取所指文件。 pub path: String, // ObservedRequest 传入 HTTP/Console 显示标签;本字段不参与认证或路由,也不在此截断。 pub origin: String, // 开始时刻的 UTC 时分秒文本,不含日期;耗时另用 started 单调时钟计算。 pub timestamp: String, // 初始为空,成功读取请求体后 body 覆盖预览;不是用于执行脚本的原始正文存储。 pub request: String, // finish 记录已构造响应的正文预览,即使后续 socket 发送失败也可能有值。 pub response: String, // None 表示尚未记录响应;完成后保存 HTTP 状态码,不据此判断业务调用是否成功。 pub status: Option, // 初始为零,finish 时从 begin 起累计毫秒;不在界面每次绘制时持续刷新。 pub elapsed_ms: u128, // 请求头已经过观察器白名单筛选,这里再截断文本;ActivityLog 自身不检查头名称。 pub request_headers: String, // respond 提供已构造响应的全部头部文本,本模块仅限长,不沿用请求头的白名单。 pub response_headers: String, // 使用单调时钟计算耗时,不受系统时间调整影响;timestamp 则用于显示 UTC 时刻。 started: Instant, } #[derive(Debug, Default)] /// 按容量维护近期 HTTP 活动与请求身份,防止无限日志积累。 pub struct ActivityLog { // 开始顺序队列,Network/Console 工具通过共享锁读取或复制;完成顺序不会重排记录。 pub records: VecDeque, // 默认从零开始,begin 先递增再使用;计数仅属此日志实例,不是跨进程请求身份。 next_id: u64, } impl ActivityLog { /// 为请求分配活动 ID 并登记其方法、来源和受限头部。 // runtime/api 的 ObservedRequest 构造时调用;先写日志再由调用方通知界面,本模块不主动唤醒窗口。 pub fn begin(&mut self, method: String, path: String, origin: String, headers: String) -> u64 { // ID 在当前日志实例内递增,不从队列长度推导;淘汰旧项不会重置计数。 self.next_id += 1; let id = self.next_id; // 显示时间取 Unix 秒数的当日余数;系统时间早于纪元时回退到零。这里只显示时间,不保存日期。 let seconds = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() % 86400; // 登记开始状态,正文和响应留空,started 与墙上时钟分别服务耗时和时间显示。 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 项范围内完成;本方法覆盖之前的请求预览,不拼接分块正文。 // ObservedRequest::record_body 在读取请求体后调用,后续 submit 仍使用原正文,日志只保存截断副本。 pub fn body(&mut self, id: u64, body: &str) { // 按 ID 更新仍在队列内的记录;请求被容量淘汰时不重建,以免旧请求挤走较新的观察记录。 if let Some(record) = self.records.iter_mut().find(|r| r.id == id) { record.request = preview(body); } } /// 为指定请求登记响应状态、正文/头部预览与单调时钟耗时;记录已淘汰时忽略。 // 仅更新仍保留的请求,响应正文和头部都重新截断;耗时从 begin 起计算。 // ObservedRequest::respond 在真实响应发送前调用,状态和耗时表示已构造响应,不证明客户端接收成功。 pub fn finish(&mut self, id: u64, status: u16, body: &str, headers: String) { // finish 可能晚于容量淘汰,找不到 ID 时整个更新跳过;不为旧响应另建记录。 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); } } } /// 截取可显示的受限文本预览,避免日志无限保留完整大正文。 // begin/body/finish 统一使用 UTF-8 字节预算;输入未超限时仍返回独立 String,不借用请求缓冲。 pub fn preview(value: &str) -> String { // 恰好等于上限也保留原文,不追加截断标记;长度判断按字节而不是字符数量。 if value.len() <= BODY_PREVIEW { return value.to_owned(); } // UTF-8 多字节字符不能从中间切开;从字节上限回退到合法边界,确保输出始终是有效字符串。 let mut end = BODY_PREVIEW; // 最多回退到当前 UTF-8 字符起点,只保证字符边界,不承诺完整字素簇边界。 while !value.is_char_boundary(end) { end -= 1; } format!("{}\n[preview truncated at 32 KiB]", &value[..end]) }