diff --git a/src/runtime/api.rs b/src/runtime/api.rs index ac91802..041cc35 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -5,6 +5,9 @@ //! main::main 调用 start,shell 持有 RuntimeControl;HTTP 请求经 handle/submit 入队,原生输入经 dispatch_native 入同一队列。 //! worker 子模块拥有实际 ReplSession;本文件的线程循环只决定继续哪个请求、何时等待、如何响应和退出。 //! 本地端点、观察器和 EventLoopProxy 都是 Tessera 宿主适配,不能把整个文件直接作为无 GUI 的 Nexus 核心。 +//! +//! 成功入队仅表示接受执行请求,最终回执由 worker 完成或明确拒绝后发出;它与发布 revision 并非同一事件。 +//! 普通请求队列和退出通道相互独立,pending 保存原请求与继续状态;退出优先于下一量子,但不由 HTTP 线程抢先操作 VM。 mod guide; mod worker; @@ -298,6 +301,7 @@ fn start_server_observed( current.document.frame.session = 0; current.document.frame.focus = None; for view in current.document.windows.values_mut() { + // 共享 VM 已清理,每个副窗保留画面都必须撤销交互会话和焦点,避免退出期间仍向旧目标投递输入。 view.frame.session = 0; view.frame.focus = None; } @@ -376,6 +380,7 @@ fn start_server_observed( worker.repl = None; } if wait.is_zero() { + // 活跃续体/协程采用零接收超时;此处仅让出线程调度机会,下一循环仍先检查退出并推进原 VM。 thread::yield_now(); } } @@ -748,6 +753,7 @@ fn submit( } // 仅完整 Code 请求分配新 submission,事件/REPL/表达式继承当前身份,保留共享域输入顺序。 let submission = match if kind == RequestKind::Code { + // 先计算候选接受号而不改 current.submission;只有下面 try_send 成功才让旧提交失去发布资格。 current.next_submission() } else { Ok(current.submission) @@ -831,6 +837,7 @@ fn read_code(request: &mut Request) -> Result { .read_to_string(&mut code) .context("read code")?; if code.len() as u64 > MAX_CODE_SIZE { + // code.len() 是 UTF-8 字节数,流读取上限多留一个字节用于探测,不能以字符数替代 1 MiB 源码预算。 anyhow::bail!("code exceeds the 1 MiB limit"); } Ok(code) diff --git a/src/runtime/api/worker.rs b/src/runtime/api/worker.rs index 6ab43a4..a522e25 100644 --- a/src/runtime/api/worker.rs +++ b/src/runtime/api/worker.rs @@ -5,6 +5,11 @@ //! api::start_server_observed 的唯一执行线程创建 Worker,每次循环调用 begin_request/resume_request 或 refresh_viewport。 //! HTTP/原生请求仍由外层持有,PendingRequest 只记录继续哪一步;本文件不重新读取请求流或回放已执行输入。 //! NamespaceState 是对外权威值,repl 是实际 VM 所有者;submission 是接受顺序,runtime_session 才表示画面仍属哪个 VM。 +//! +//! 请求阶段不能互换:BeforeInput 尚未执行业务,可重新进入 begin_request;Input 已执行,只能恢复视觉工作; +//! Candidate 持有独立候选,成功前不替换 self.repl。外层 api 循环保留原 job,因此这里不会创建第二份回应端。 +//! 接受 PUT 先改变 submission,候选成功提交后才切换 runtime_session;preserve_live_owner 在两者之间保留仍权威的旧域。 +//! 源业务与视觉方法在 Namespace 锁外执行,文档/命令发布回到写锁内复核身份;错误响应和故障画面发布是两项不同结果。 use super::*; // 响应已编码成内存字节,外层 RequestReply 可发 HTTP 或转换成原生回执,不需要读取 VM 返回值。 @@ -376,6 +381,7 @@ impl Worker { current.document.frame.session = 0; current.document.frame.focus = None; for document in current.document.windows.values_mut() { + // 重置的是共享堆,所有副窗旧身份都失效;保留的是静态绘制文档,不是仍可接收事件的窗口业务对象。 document.frame.session = 0; document.frame.focus = None; } @@ -388,6 +394,7 @@ impl Worker { serde_json::json!({"ok":true,"execution":"repl-reset","persistent":true,"session_reset":true,"message":"Console memory cleared; displayed page preserved"}), )); } + // 旧布局/绘制仍挂起时只保存等待阶段,不能在它的只读 VM 调用中执行新的 REPL 或事件副作用。 if self .repl .as_ref() @@ -414,6 +421,7 @@ impl Worker { } } }; + // 先按输入种类筛选再查资源能力:合法 JSON 不代表有权触发任意名字的宿主操作。 if !input.kind.starts_with("developer-open:") && !matches!( input.kind.as_str(), @@ -638,6 +646,7 @@ impl Worker { // Console 不是事件,不制造一个空 Input;下面按 source 执行同一持久会话的交互语句。 None }; + // 只有当前 epoch 没有实际会话才惰性创建空 Console;正常输入沿用已有 VM,不为每次 POST 复制 globals。 if self .repl .as_ref() @@ -935,6 +944,7 @@ impl Worker { /// 新提交/候选过期或失败时调用,若旧 VM 仍拥有发布画面则只跟进 epoch;DELETE 已撤权威则丢弃它。 pub(super) fn preserve_live_owner(&mut self, current: &NamespaceState) { + // 调用方已持发布状态锁,本函数只对照身份修改 worker 本地拥有关系,不执行回调或生成新文档。 if let Some((epoch, runtime)) = &mut self.repl { if current.runtime_session == runtime.id { // 旧程序还是真正交互域,接受次序更新但其实际对象和 session 身份都不改变。 @@ -1071,10 +1081,12 @@ impl Worker { let Ok(mut current) = namespace.write() else { return error_response(StatusCode(500), "namespace lock poisoned"); }; + // 候选计算结束不代表仍被接受;先处理过期并保留权威旧域,再考虑把候选结果交给 commit。 if current.submission != job.submission { self.preserve_live_owner(¤t); return superseded(job.submission); } + // 失败分支遵循同一原则:候选未被完整接受就不把其 VM 装进 self.repl,不用半成品替换旧程序。 match result { Ok(Ok((runtime, document, commands))) => match current.commit(job.submission, document) { diff --git a/src/runtime/events.rs b/src/runtime/events.rs index 9f381a4..e461d52 100644 --- a/src/runtime/events.rs +++ b/src/runtime/events.rs @@ -5,6 +5,10 @@ //! shell::app 建立 Client 并发送捕获到的 Input;Client 后台 relay 经 api::RuntimeControl::dispatch_native 交给 worker。 //! HTTP 路由也反序列化同一 Input 并调用 validate,但不会走原生 relay 的连续输入版本刷新。 //! 这里只合并同目标相邻 pointermove 的绝对采样;滚动/相机的增量逐条保留,不能用最后一个覆盖累计移动。 +//! +//! 队列合并和版本追随是两个独立规则:仅 pointermove 能覆盖队尾采样;原始 pointerdown/up/leave/cancel 也可追随版本, +//! 但仍逐条保持顺序。高层 click 不追随,必须用捕获时的按下/释放与几何版本重新确认目标。 +//! 版本追随只确认原 window/session/Mount 及其能力仍有效,不做重新命中,也不把旧目标换成当前光标下的新对象。 use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, @@ -277,6 +281,7 @@ impl Inbox { // wait 原子放锁并等待;GUI 生产者可入队,通知后重新取得同一状态锁。 q = self.ready.wait(q).ok()?; } + // 关闭优先于残留项;正常唤醒后只取队首一个事件,relay 完成这条 worker 回执后才会再次 receive。 if q.closed { None } else { q.items.pop_front() } } } @@ -304,6 +309,7 @@ impl Client { while let Some(mut input) = receiver.receive() { // 每条输入出队时再看最新权威发布帧,连续采样不沿用排队前已经过时的布局版本。 if let Ok(current) = namespace.read() { + // 这里只更新允许类别的 layout_version;下面 worker 接纳输入时还会再次读取权威帧,覆盖 relay 排队期间的新变化。 if !refresh_continuous_input(&mut input, ¤t) { // 目标消失/换会话时丢弃,不能把旧手势重定向到同位置的新出现。 continue; @@ -329,6 +335,7 @@ impl Client { return false; }; if q.closed { + // 最后一个生产者已关闭队列,不能因新的 send 再唤醒 relay 并恢复过期输入流。 return false; } // enqueue 的覆盖合并也算接收成功,relay 会读取最终最新样本。