merge: resolve manuscript conflicts with latest main comments
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
use super::*;
|
||||
|
||||
type HttpResponse = Response<std::io::Cursor<Vec<u8>>>;
|
||||
// 外层 thread::Result 区分 Rust panic,内层 Result 区分可诊断编译/运行错误;正常结果另带待发布窗口命令。
|
||||
type InputOutcome =
|
||||
std::thread::Result<Result<(endor_host::ReplResult, Vec<crate::window::Command>)>>;
|
||||
|
||||
@@ -19,6 +20,7 @@ type CandidateOutcome = std::thread::Result<
|
||||
|
||||
/// 已执行业务部分但仍在等待展示刷新的输入元数据,避免恢复时重放回调。
|
||||
pub(super) struct PendingInput {
|
||||
// 输入开始时所属 submission,与 id(VM 会话)分开;新 PUT 接受时可保留正在执行的原 VM 至输入收敛。
|
||||
epoch: u64,
|
||||
id: u64,
|
||||
event: bool,
|
||||
@@ -27,6 +29,7 @@ pub(super) struct PendingInput {
|
||||
|
||||
/// 挂起 HTTP 请求的阶段:等旧刷新、继续输入或继续隔离候选。
|
||||
pub(super) enum PendingRequest {
|
||||
// 仅等待旧只读刷新结束,业务输入尚未执行;恢复时可重新走入口,而 Input 阶段不能重放业务副作用。
|
||||
BeforeInput,
|
||||
Input(Box<PendingInput>),
|
||||
Candidate(Box<endor_host::ReplSession>),
|
||||
@@ -56,6 +59,7 @@ fn current_viewports(
|
||||
})
|
||||
.map(|(id, size)| (*id, *size))
|
||||
.collect();
|
||||
// 主窗口逻辑视口优先覆盖 map 同名值,主壳可以跨会话沿用;副窗口必须仍属于当前发布世代。
|
||||
if let Some(viewport) = current.viewport {
|
||||
viewports.insert(crate::window::MAIN_WINDOW, viewport);
|
||||
}
|
||||
@@ -77,6 +81,7 @@ impl Worker {
|
||||
error: None,
|
||||
};
|
||||
};
|
||||
// 正常域允许有界关闭回调,故障域只做宿主清理;catch_unwind 防止清理 panic 跳过原生退出通知。
|
||||
let callbacks = !runtime.is_faulted();
|
||||
let result = catch_unwind(AssertUnwindSafe(|| runtime.shutdown()));
|
||||
match result {
|
||||
@@ -108,6 +113,7 @@ impl Worker {
|
||||
namespace: &Arc<RwLock<NamespaceState>>,
|
||||
notify: &Notify,
|
||||
) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
// 故障/失活资源的专用关闭只处理副窗口,主壳由 OS 关闭控制走全应用退出流程。
|
||||
if input.window == crate::window::MAIN_WINDOW {
|
||||
return error_response(
|
||||
StatusCode(409),
|
||||
@@ -126,6 +132,7 @@ impl Worker {
|
||||
.1
|
||||
.retire_window(input.window)
|
||||
}));
|
||||
// 退役失败不能继续借用半清理 VM,放弃整个活域并在下方清其他帧的会话与命令资格。
|
||||
if !matches!(result, Ok(Ok(()))) {
|
||||
// Never keep a half-cleaned VM live or borrow another generation.
|
||||
self.repl = None;
|
||||
@@ -141,6 +148,7 @@ impl Worker {
|
||||
let Some(view) = current.document.windows.get(&input.window) else {
|
||||
return error_response(StatusCode(409), "window is no longer active");
|
||||
};
|
||||
// 即便副窗口数值 ID 相同,也不能让旧会话的关闭请求删除新会话投影。
|
||||
if view.frame.session != input.session {
|
||||
return error_response(
|
||||
StatusCode(409),
|
||||
@@ -200,6 +208,7 @@ impl Worker {
|
||||
let Some((owner, runtime)) = &mut self.repl else {
|
||||
return;
|
||||
};
|
||||
// submission 变化先尝试保留仍权威的活域所有者,不立即拿旧 epoch 继续发布后台结果。
|
||||
if *owner != epoch {
|
||||
if let Ok(current) = namespace.read() {
|
||||
self.preserve_live_owner(¤t);
|
||||
@@ -218,11 +227,13 @@ impl Worker {
|
||||
}
|
||||
// An accepted foreground input waits only for the existing
|
||||
// refresh; do not keep placing new callbacks ahead of it.
|
||||
// 已有前台请求只等当前刷新,不不断把新任务回调插到它前面,避免忙后台工作饿死用户输入。
|
||||
let completed = if before_input {
|
||||
None
|
||||
} else {
|
||||
runtime.poll_tasks_incremental(&viewports)?
|
||||
};
|
||||
// 任务后续刷新未收敛时可以发布较早已完整的 resize 文档,但暂扣任务产生的命令,防止半完成副作用外泄。
|
||||
if runtime.has_pending_refresh() {
|
||||
// A complete earlier resize may publish; task effects and their
|
||||
// native commands wait until the task's refresh has settled.
|
||||
@@ -235,6 +246,7 @@ impl Worker {
|
||||
Ok(current) => current,
|
||||
Err(_) => return,
|
||||
};
|
||||
// 耗时 VM 工作期间 HTTP 线程可能接受 PUT/DELETE,发布写锁内再核对 submission 与 session 双身份。
|
||||
if current.submission != epoch || current.runtime_session != id {
|
||||
return;
|
||||
}
|
||||
@@ -243,6 +255,7 @@ impl Worker {
|
||||
if let Some(document) = document {
|
||||
(document, Some(commands))
|
||||
} else {
|
||||
// 无新文档也可能有标题/焦点等原生命令;只在实际有投影变化时唤醒 GUI,避免空闲无效重绘。
|
||||
let projected = !commands.is_empty();
|
||||
current.publish_window_commands(id, commands);
|
||||
drop(current);
|
||||
@@ -259,6 +272,7 @@ impl Worker {
|
||||
.chars()
|
||||
.take(1024)
|
||||
.collect(),
|
||||
// 内部 panic 与普通背景错误不同,必须丢弃 VM 并撤销所有原生命令,不能继续运行损坏的域。
|
||||
_ => {
|
||||
self.repl = None;
|
||||
current.runtime_session = 0;
|
||||
@@ -289,6 +303,7 @@ impl Worker {
|
||||
namespace: &Arc<RwLock<NamespaceState>>,
|
||||
notify: &Notify,
|
||||
) -> RequestProgress {
|
||||
// 完整 PUT 独立候选,表达式隔离求值;只有 REPL/事件会进入当前持久域并等待已有刷新。
|
||||
if job.kind == RequestKind::Code {
|
||||
return self.begin_program_request(job, namespace, notify);
|
||||
}
|
||||
@@ -311,6 +326,7 @@ impl Worker {
|
||||
if epoch != job.submission {
|
||||
return RequestProgress::Complete(superseded(job.submission));
|
||||
}
|
||||
// 重置丢弃内存会话但保留显示文档,清帧 session/focus 使旧画面不再接受旧对象事件。
|
||||
if job.kind == RequestKind::ResetRepl {
|
||||
self.repl = None;
|
||||
if let Ok(mut current) = namespace.write() {
|
||||
@@ -336,6 +352,7 @@ impl Worker {
|
||||
{
|
||||
return RequestProgress::Pending(PendingRequest::BeforeInput);
|
||||
}
|
||||
// 事件先做 JSON 结构、kind 白名单和 Input 字段校验,再用权威发布帧核对具体资源能力。
|
||||
let event = if job.kind == RequestKind::Event {
|
||||
let mut input: crate::events::Input = if let Some(input) = &job.native {
|
||||
input.clone()
|
||||
@@ -413,6 +430,7 @@ impl Worker {
|
||||
};
|
||||
&document.frame
|
||||
};
|
||||
// 窗口关闭允许处理已失活/故障副窗口,不像点击一样要求可运行 VM;由专用宿主退役路径处理。
|
||||
if input.kind == "window-close"
|
||||
&& frame.window == input.window
|
||||
&& frame.session == input.session
|
||||
@@ -433,6 +451,7 @@ impl Worker {
|
||||
);
|
||||
}
|
||||
}
|
||||
// 标签拖放同时验证目标窗口的会话、版本、无故障及插入位置,源窗口校验不能替代目标校验。
|
||||
if let Some(target) = &input.tab_drop {
|
||||
let target_frame = if target.window == crate::window::MAIN_WINDOW {
|
||||
Some(¤t.document.frame)
|
||||
@@ -460,6 +479,7 @@ impl Worker {
|
||||
));
|
||||
}
|
||||
}
|
||||
// 统一事件归属门:窗口/session/版本及操作对应资源必须都合法;window-close 特别允许旧布局版本。
|
||||
if frame.window != input.window
|
||||
|| frame.session != input.session
|
||||
|| (input.kind != "window-close" && frame.layout_version != input.layout_version)
|
||||
@@ -473,6 +493,7 @@ impl Worker {
|
||||
input.kind.as_str(),
|
||||
"tab-activate" | "tab-close" | "tab-detach" | "tab-drop"
|
||||
) {
|
||||
// 标签操作的 mount 字段承载 Tab ID,而普通输入承载 Mount ID;两套身份不能混着查询。
|
||||
!frame.tabs.iter().any(|tab| tab.id == input.mount)
|
||||
} else {
|
||||
!frame.mounts.iter().any(|mount| {
|
||||
@@ -480,6 +501,7 @@ impl Worker {
|
||||
&& if input.kind.starts_with("viewport-") {
|
||||
mount.canvas_viewport
|
||||
} else if input.kind.starts_with("scene-") {
|
||||
// 3D 手势还要求显式控制器能力,scroll 要有滚动投影,点击/焦点则要求 interactive。
|
||||
mount.scene3d_viewport && mount.camera_controls
|
||||
} else if input.kind == "scroll" {
|
||||
mount.scroll.is_some()
|
||||
@@ -499,6 +521,7 @@ impl Worker {
|
||||
"event targets an obsolete or inactive frame",
|
||||
));
|
||||
}
|
||||
// 兼容页面仅 Canvas 可缩放,Window 模式不接受 pan;结构合法不等于当前页面模式支持操作。
|
||||
if input.kind == "page-zoom" && frame.surface.mode != crate::page::Mode::Canvas
|
||||
|| input.kind == "page-pan" && frame.surface.mode == crate::page::Mode::Window
|
||||
{
|
||||
@@ -507,6 +530,7 @@ impl Worker {
|
||||
"camera input is unavailable in this Page mode",
|
||||
));
|
||||
}
|
||||
// 缩放锚点须经过面板反投影与内容 clip 检查,不能落在视口边框或被遮挡面板上仍缩放。
|
||||
if matches!(input.kind.as_str(), "viewport-zoom" | "scene-zoom")
|
||||
&& !frame.mounts.iter().any(|mount| {
|
||||
mount.id == input.mount
|
||||
@@ -529,6 +553,7 @@ impl Worker {
|
||||
"zoom anchor is outside content viewport",
|
||||
));
|
||||
}
|
||||
// 世界 UI 的非线性位移变换需要位置锚点;普通二维视口的线性位移可以省略 point。
|
||||
if (input.kind.starts_with("viewport-") || input.kind.starts_with("scene-"))
|
||||
&& input.point.is_none()
|
||||
&& frame
|
||||
@@ -550,6 +575,7 @@ impl Worker {
|
||||
.as_ref()
|
||||
.is_none_or(|(current, _)| *current != epoch)
|
||||
{
|
||||
// 没有当前 epoch 会话时惰性建立空 REPL,每次分配新会话 ID 并检查溢出,禁止复用旧输入身份。
|
||||
self.next_session = match self.next_session.checked_add(1) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
@@ -594,6 +620,7 @@ impl Worker {
|
||||
} else {
|
||||
session.execute_incremental(&job.source, &job.namespace, &viewports)?
|
||||
};
|
||||
// 业务执行可能已改 VM,但展示未完成时暂不取窗口命令;PendingInput 保存结果并只恢复展示阶段。
|
||||
let commands = if session.has_pending_refresh() {
|
||||
Vec::new()
|
||||
} else {
|
||||
@@ -611,6 +638,7 @@ impl Worker {
|
||||
result,
|
||||
})));
|
||||
}
|
||||
// 启动失败且残留挂起刷新时直接丢弃域,不能保留一个无法安全推进的 HTTP 请求。
|
||||
// A failed start must never leave a request waiting on a broken VM.
|
||||
self.repl = None;
|
||||
}
|
||||
@@ -652,6 +680,7 @@ impl Worker {
|
||||
notify: &Notify,
|
||||
) -> RequestProgress {
|
||||
let viewports = match namespace.read() {
|
||||
// 正在执行输入只要仍属当前提交或仍拥有权威 VM 就可收敛;两项皆失效才彻底废弃。
|
||||
Ok(current)
|
||||
if current.submission == input.epoch || current.runtime_session == input.id =>
|
||||
{
|
||||
@@ -679,6 +708,7 @@ impl Worker {
|
||||
return RequestProgress::Complete(superseded(job.submission));
|
||||
}
|
||||
let resumed = catch_unwind(AssertUnwindSafe(|| -> Result<_> {
|
||||
// 这里只恢复 pending 展示/尺寸工作,不再调用 execute/dispatch,所以前一个量子的源副作用不会重放。
|
||||
let document = session.resize_windows_incremental(&viewports, false)?;
|
||||
if session.has_pending_refresh() {
|
||||
return Ok(None);
|
||||
@@ -723,6 +753,7 @@ impl Worker {
|
||||
let Ok(mut current) = namespace.write() else {
|
||||
return error_response(StatusCode(500), "namespace lock is poisoned");
|
||||
};
|
||||
// 接受新 PUT 会使排队旧输入失效,但已执行的活域输入需先收敛,才能在候选失败时留下内部一致的旧程序。
|
||||
let epoch = if current.submission != epoch {
|
||||
if current.runtime_session != id {
|
||||
drop(current);
|
||||
@@ -744,6 +775,7 @@ impl Worker {
|
||||
match result {
|
||||
Ok(Ok((result, commands))) => {
|
||||
let projected = !commands.is_empty();
|
||||
// rendered 只表示此次交付带新文档,成功执行但页面未变仍可返回 value/session;不因此虚增 revision。
|
||||
let rendered = result.document.is_some();
|
||||
if let Some(document) = result.document {
|
||||
match current.commit(epoch, document) {
|
||||
@@ -769,6 +801,7 @@ impl Worker {
|
||||
// Preserve the first failure of this VM; subsequent
|
||||
// rejected Console input must not hide the callback cause.
|
||||
// A preserved picture from another generation is unrelated.
|
||||
// 同 VM 保留最初故障原因,后续“域已停止”错误不能覆盖真正回调故障;其他世代旧图不沿用故障。
|
||||
let fault = (document.frame.session == id)
|
||||
.then(|| document.frame.fault.clone())
|
||||
.flatten()
|
||||
@@ -790,6 +823,7 @@ impl Worker {
|
||||
for view in document.windows.values_mut() {
|
||||
view.frame.fault = document.frame.fault.clone();
|
||||
}
|
||||
// 输入 panic 保留最近画面但撤销运行时/命令并发布故障条,再将 self.repl 丢弃。
|
||||
current.runtime_session = 0;
|
||||
current.window_commands.clear();
|
||||
let _ = current.commit(epoch, document);
|
||||
@@ -847,6 +881,7 @@ impl Worker {
|
||||
}
|
||||
};
|
||||
let id = self.next_session;
|
||||
// 候选装载运行在独立 ReplSession,未成功前不修改 self.repl,旧程序仍是权威展示来源。
|
||||
let started = catch_unwind(AssertUnwindSafe(|| {
|
||||
endor_host::ReplSession::load_incremental(
|
||||
&job.source,
|
||||
@@ -857,6 +892,7 @@ impl Worker {
|
||||
)
|
||||
}));
|
||||
let result = match started {
|
||||
// 候选业务装载成功但布局未完成,整个候选移进 PendingRequest;不能把 None 当成空文档提交。
|
||||
Ok(Ok((runtime, None))) => {
|
||||
return RequestProgress::Pending(PendingRequest::Candidate(Box::new(runtime)));
|
||||
}
|
||||
@@ -882,6 +918,7 @@ impl Worker {
|
||||
Ok(current) if current.submission == job.submission => {
|
||||
// Secondary native resources still belong to the old VM. Only
|
||||
// the primary shell's current viewport applies to a candidate.
|
||||
// 候选不沿用旧副窗口的视口;副窗口资源归旧 VM,只有主壳当前尺寸可作为新候选分配条件。
|
||||
current
|
||||
.viewport
|
||||
.map(|size| (crate::window::MAIN_WINDOW, size))
|
||||
@@ -905,6 +942,7 @@ impl Worker {
|
||||
if runtime.has_pending_refresh() {
|
||||
return Ok(None);
|
||||
}
|
||||
// 所有刷新结束仍没有 Page 文档时明确失败,不用旧显示值冒充候选自己的输出。
|
||||
let document =
|
||||
document.ok_or_else(|| anyhow::anyhow!("program did not publish a Page"))?;
|
||||
Ok(Some((document, runtime.take_window_commands()?)))
|
||||
@@ -937,6 +975,7 @@ impl Worker {
|
||||
match result {
|
||||
Ok(Ok((runtime, document, commands))) => match current.commit(job.submission, document)
|
||||
{
|
||||
// 写锁内先原子提交文档,再切换命令世代与活域;解锁后唤醒 GUI,新 VM 至此才取代旧 VM。
|
||||
Ok(Some(revision)) => {
|
||||
current.publish_window_commands(id, commands);
|
||||
self.repl = Some((job.submission, runtime));
|
||||
@@ -953,6 +992,7 @@ impl Worker {
|
||||
failed => {
|
||||
// The candidate never mutated the displayed program's heap.
|
||||
// Restore that program's event epoch when a replacement fails.
|
||||
// 候选失败只更新旧活域 epoch,保留其堆与页面;不存在旧域时也不会借用其他会话伪造成功。
|
||||
self.preserve_live_owner(¤t);
|
||||
drop(current);
|
||||
match failed {
|
||||
@@ -973,6 +1013,7 @@ fn build(
|
||||
namespace: &Arc<RwLock<NamespaceState>>,
|
||||
notify: &Notify,
|
||||
) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
// 隔离表达式不读写持久 REPL 堆,也不发布文档,响应明确 persistent=false。
|
||||
if job.kind == RequestKind::Expression {
|
||||
return match catch_unwind(AssertUnwindSafe(|| endor_host::evaluate(&job.source))) {
|
||||
Ok(Ok(value)) => json(
|
||||
@@ -990,6 +1031,7 @@ fn build(
|
||||
}
|
||||
// Only an independent native snapshot crosses into the published state.
|
||||
// A fresh VM is discarded on success, error and unwind alike.
|
||||
// 一次性构建同样用独立 VM,计算锁外执行,返回后再次核对 submission 才能发布原生快照。
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
endor_host::compile(&job.source, &job.namespace, job.submission)
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user