1181 lines
62 KiB
Rust
1181 lines
62 KiB
Rust
//! 唯一串行执行 VM 的 API worker。保留活跃运行时,另建 PUT 候选,并在候选完整成功后切换发布。
|
||
//! 输入、视口刷新与候选布局可以跨 worker 轮次挂起;中途只公布完整文档,不能让新输入进入未结束的只读调用。
|
||
//! 事件身份同时校验窗口、session、layout_version 和 Mount 能力。失败、替换与关闭分别处理,避免旧回调或旧命令泄漏到新 VM。
|
||
//!
|
||
//! 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 返回值。
|
||
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>)>>;
|
||
|
||
// 候选成功必须成套交回持久 VM、完整文档及命令;不能只返回快照后丢掉事件需要的真实运行时。
|
||
type CandidateOutcome = std::thread::Result<
|
||
Result<(
|
||
endor_host::ReplSession,
|
||
crate::document::RenderDocument,
|
||
Vec<crate::window::Command>,
|
||
)>,
|
||
>;
|
||
|
||
/// 已执行业务部分但仍在等待展示刷新的输入元数据,避免恢复时重放回调。
|
||
pub(super) struct PendingInput {
|
||
// 输入开始时所属 submission,与 id(VM 会话)分开;新 PUT 接受时可保留正在执行的原 VM 至输入收敛。
|
||
epoch: u64,
|
||
// 实际运行时代际,和 epoch 一起核验;接受新 PUT 不代表已经切换到新 VM。
|
||
id: u64,
|
||
// 决定响应 execution 和错误停域规则,恢复展示时不再重新猜原始输入类别。
|
||
event: bool,
|
||
// 已经执行得到的值/统计,后续只补 document,不重复 callback 或 execute 取相同结果。
|
||
result: endor_host::ReplResult,
|
||
}
|
||
|
||
/// 挂起 HTTP 请求的阶段:等旧刷新、继续输入或继续隔离候选。
|
||
pub(super) enum PendingRequest {
|
||
// 仅等待旧只读刷新结束,业务输入尚未执行;恢复时可重新走入口,而 Input 阶段不能重放业务副作用。
|
||
BeforeInput,
|
||
// 输入业务部分已执行,保留活 repl 并只恢复该输入的视觉刷新。
|
||
Input(Box<PendingInput>),
|
||
// 新 PUT 的隔离候选还未发布,活 repl 仍保留旧程序,不能提前替换。
|
||
Candidate(Box<endor_host::ReplSession>),
|
||
}
|
||
|
||
/// 一次 worker 推进的结果:继续保留请求或交付最终响应。
|
||
pub(super) enum RequestProgress {
|
||
// 外层将原 job 和继续状态一起保存,下一量子进入 resume_request。
|
||
Pending(PendingRequest),
|
||
// 最终响应值,外层消费原 RequestReply 一次性回应,不再继续这个请求。
|
||
Complete(HttpResponse),
|
||
}
|
||
|
||
/// 空闲刷新/输入/恢复路径在发布状态读锁内调用,筛出当前运行时合法尺寸,不执行 VM 或修改窗口表。
|
||
fn current_viewports(
|
||
current: &NamespaceState,
|
||
) -> std::collections::BTreeMap<u64, crate::layout::Size> {
|
||
// 主壳可跨 VM 使用;副窗必须仍在当前 document 且属于 runtime_session,旧数字 ID 本身不算归属。
|
||
let mut viewports: std::collections::BTreeMap<_, _> = current
|
||
.window_viewports
|
||
.iter()
|
||
.filter(|(id, _)| {
|
||
**id == crate::window::MAIN_WINDOW
|
||
|| (current.runtime_session != 0
|
||
&& current
|
||
.document
|
||
.windows
|
||
.get(id)
|
||
.is_some_and(|view| view.frame.session == current.runtime_session))
|
||
})
|
||
.map(|(id, size)| (*id, *size))
|
||
.collect();
|
||
// 主窗口逻辑视口优先覆盖 map 同名值,主壳可以跨会话沿用;副窗口必须仍属于当前发布世代。
|
||
if let Some(viewport) = current.viewport {
|
||
viewports.insert(crate::window::MAIN_WINDOW, viewport);
|
||
}
|
||
viewports
|
||
}
|
||
|
||
#[derive(Default)]
|
||
/// 唯一活跃 VM 的串行拥有者,另行管理候选和下一 session 身份。
|
||
pub(super) struct Worker {
|
||
// 元组第一项是当前接受提交 epoch,第二项是真实 VM;新 PUT 接受时旧 VM 仍可能保持权威。
|
||
pub(super) repl: Option<(u64, endor_host::ReplSession)>,
|
||
// 单调 session 分配高水位,即使候选失败也不复用已经分配的代际身份。
|
||
pub(super) next_session: u64,
|
||
}
|
||
impl Worker {
|
||
/// api 线程循环收到退出信号时调用,移出活运行时并生成纯文本 ShutdownOutcome,供原生退出报告。
|
||
pub(super) fn shutdown_runtime(&mut self) -> ShutdownOutcome {
|
||
// take 先撤 worker 的活域拥有关系,没有域时仍返回明确成功摘要而非伪造清理数量。
|
||
let Some((_, mut runtime)) = self.repl.take() else {
|
||
return ShutdownOutcome {
|
||
summary: "Tessera shutdown: no live runtime".into(),
|
||
error: None,
|
||
};
|
||
};
|
||
// 正常域允许有界关闭回调,故障域只做宿主清理;catch_unwind 防止清理 panic 跳过原生退出通知。
|
||
let callbacks = !runtime.is_faulted();
|
||
// 普通清理错误与 Rust panic 都转换成结果,不能让执行线程提前消失而使 GUI 永远等不到完成通知。
|
||
let result = catch_unwind(AssertUnwindSafe(|| runtime.shutdown()));
|
||
match result {
|
||
Ok(Ok((windows, mounts, tasks))) => ShutdownOutcome {
|
||
summary: format!(
|
||
"Tessera shutdown: {windows} windows, {mounts} mounts, {tasks} task entries; callbacks={callbacks}"
|
||
),
|
||
error: None,
|
||
},
|
||
result => {
|
||
// 内部 panic 没有可安全继续执行的源错误对象,单独说明;正常 Err 则保留错误链。
|
||
let error = match result {
|
||
Ok(Err(error)) => format!("shutdown cleanup failed: {error:#}"),
|
||
_ => "shutdown cleanup failed after an internal panic".into(),
|
||
};
|
||
// 按 Unicode 字符限长,不切坏 UTF-8,也不把无限错误链塞入退出消息。
|
||
let error: String = error.chars().take(4096).collect();
|
||
ShutdownOutcome {
|
||
summary: error.clone(),
|
||
error: Some(error),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// begin_request 的故障/失活副窗 window-close 分支调用,做纯宿主退役,不运行任何源关闭/生命周期回调。
|
||
pub(super) fn close_inactive_window(
|
||
&mut self,
|
||
input: &crate::events::Input,
|
||
epoch: u64,
|
||
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),
|
||
"close the primary shell with its OS window control",
|
||
);
|
||
}
|
||
// 只有 epoch/session 都匹配才清该 VM 内部资源;相同窗口 ID 不授权操作另一个会话。
|
||
let owns_runtime = self.repl.as_ref().is_some_and(|(owner, runtime)| {
|
||
*owner == epoch && runtime.id == input.session && input.session != 0
|
||
});
|
||
// 退役内部失败时整个域被丢弃,后面需同时撤其他帧的会话/命令资格。
|
||
let mut discarded = false;
|
||
if owns_runtime {
|
||
// 接住退役 panic,保证后续仍能从共享文档删除原生副窗投影,而不保留半清理 VM。
|
||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||
self.repl
|
||
.as_mut()
|
||
.expect("owned runtime")
|
||
.1
|
||
.retire_window(input.window)
|
||
}));
|
||
// 退役失败不能继续借用半清理 VM,放弃整个活域并在下方清其他帧的会话与命令资格。
|
||
if !matches!(result, Ok(Ok(()))) {
|
||
// 不借用其他代际补清理;放弃整个 repl 后下方只操作最近发布的纯数据文档。
|
||
self.repl = None;
|
||
discarded = true;
|
||
}
|
||
}
|
||
// VM 清理在锁外执行完才申请发布写锁,重新验证期间是否有 PUT/DELETE 接受。
|
||
let Ok(mut current) = namespace.write() else {
|
||
return error_response(StatusCode(500), "namespace lock poisoned");
|
||
};
|
||
// 锁外退役期间可能接受更新提交;过期请求不能再删除当前世代文档中的同编号窗口。
|
||
if current.submission != epoch {
|
||
return superseded(epoch);
|
||
}
|
||
// 只查副窗投影,原窗口已消失就返回冲突,不为关闭旧事件重建一个资源。
|
||
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),
|
||
"window belongs to another runtime generation",
|
||
);
|
||
}
|
||
// 在完整已发布文档副本上删目标,保留其他窗口画面;commit 再验证整份跨线程值。
|
||
let mut document = current.document.clone();
|
||
document.windows.remove(&input.window);
|
||
if discarded {
|
||
// 保留静态画面但所有交互身份失效,不能让旧按钮继续指向已丢弃的托管堆。
|
||
document.frame.session = 0;
|
||
document.frame.focus = None;
|
||
for view in document.windows.values_mut() {
|
||
// 丢弃的是整个共享 VM,所以剩余每个副窗都撤销交互身份,不能只清主画面或刚关闭的那个窗口。
|
||
view.frame.session = 0;
|
||
view.frame.focus = None;
|
||
}
|
||
current.runtime_session = 0;
|
||
current.window_commands.clear();
|
||
}
|
||
// 发布退役后的完整文档;只有 Some 才继续移除视口记录并唤醒 GUI,过期或校验失败都提前返回。
|
||
match current.commit(epoch, document) {
|
||
Ok(Some(_)) => {}
|
||
Ok(None) => return superseded(epoch),
|
||
Err(error) => return error_response(StatusCode(500), error.to_string()),
|
||
}
|
||
current.window_viewports.remove(&input.window);
|
||
drop(current);
|
||
notify();
|
||
json(
|
||
StatusCode(200),
|
||
serde_json::json!({"ok":true,"execution":"window-retire","window":input.window,"callbacks":false,"runtime_discarded":discarded}),
|
||
)
|
||
}
|
||
|
||
/// api 的空闲 worker 轮调用,允许推进尺寸和任务回调;有前台输入等待时使用 before_input=true 的变体。
|
||
pub(super) fn refresh_viewport(
|
||
&mut self,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
) {
|
||
self.refresh_viewport_mode(namespace, notify, false);
|
||
}
|
||
|
||
/// 空闲循环与 begin_request 共用推进器,锁外执行 VM、写锁内核对 epoch/session 后再发布,防止晚到覆盖。
|
||
pub(super) fn refresh_viewport_mode(
|
||
&mut self,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
before_input: bool,
|
||
) {
|
||
// 在读锁内取得一个一致的提交身份和尺寸快照;耗时 VM 工作不占用共享状态写锁。
|
||
let (epoch, viewports) = match namespace.read() {
|
||
Ok(current) => {
|
||
// 同一读锁下复制身份和合法尺寸,避免使用来自不同发布世代的两个快照。
|
||
let viewports = current_viewports(¤t);
|
||
(current.submission, viewports)
|
||
}
|
||
Err(_) => return,
|
||
};
|
||
// 无持久运行时就没有任务/尺寸可推进;GET 的旧静态文档本身不创造 VM。
|
||
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);
|
||
}
|
||
return;
|
||
}
|
||
// 保存执行前真实 VM 身份,发布时重新检查,不能只核对新的 submission。
|
||
let id = runtime.id;
|
||
// 耗时刷新不持 namespace 锁,HTTP 可接受新请求;普通错误/panic 留到写锁阶段区分处理。
|
||
let result = catch_unwind(AssertUnwindSafe(|| -> Result<_> {
|
||
if runtime.is_faulted() {
|
||
// 故障域 poll_tasks 仅取消/回收旧任务,不再启动新的用户回调。
|
||
runtime.poll_tasks()?;
|
||
return Ok((None, Vec::new()));
|
||
}
|
||
// 先推进已有视觉工作/尺寸,再考虑新任务,不能在只读续体里插入业务写入。
|
||
let resized = runtime.resize_windows_incremental(&viewports, false)?;
|
||
if runtime.has_pending_refresh() {
|
||
return Ok((None, Vec::new()));
|
||
}
|
||
// 已有前台请求只等当前刷新,不不断把新任务回调插到它前面,避免忙后台工作饿死用户输入。
|
||
let completed = if before_input {
|
||
// 前台输入的优先等待边界,不再让不断就绪的后台任务排在用户输入前。
|
||
None
|
||
} else {
|
||
// 真正空闲才开启一个任务推进,任务引起的展示仍可挂起等待后续量子。
|
||
runtime.poll_tasks_incremental(&viewports)?
|
||
};
|
||
// 任务后续刷新未收敛时可以发布较早已完整的 resize 文档,但暂扣任务产生的命令,防止半完成副作用外泄。
|
||
if runtime.has_pending_refresh() {
|
||
// resized 已完整可以返回,任务新产生窗口命令仍扣留至其完整刷新结束。
|
||
return Ok((resized, Vec::new()));
|
||
}
|
||
Ok((completed.or(resized), runtime.take_window_commands()?))
|
||
}));
|
||
// 计算结束后重新核对归属:期间若发生 PUT/DELETE,不能把旧域草稿发布出去。
|
||
let mut current = match namespace.write() {
|
||
Ok(current) => current,
|
||
Err(_) => return,
|
||
};
|
||
// 耗时 VM 工作期间 HTTP 线程可能接受 PUT/DELETE,发布写锁内再核对 submission 与 session 双身份。
|
||
if current.submission != epoch || current.runtime_session != id {
|
||
return;
|
||
}
|
||
// success 可仅有命令而无新帧;错误发布故障条,panic 还要丢弃 VM 与命令世代。
|
||
let (document, commands) = match result {
|
||
Ok(Ok((document, commands))) => {
|
||
// 背景轮询成功有两类结果:有新文档则走下面原子提交,无文档则仅发布可能存在的窗口命令。
|
||
if let Some(document) = document {
|
||
(document, Some(commands))
|
||
} else {
|
||
// 无新文档也可能有标题/焦点等原生命令;只在实际有投影变化时唤醒 GUI,避免空闲无效重绘。
|
||
let projected = !commands.is_empty();
|
||
current.publish_window_commands(id, commands);
|
||
drop(current);
|
||
// 已释放 Namespace 写锁后才唤醒 GUI;没有命令也没有新帧时保持空闲,不凭轮询次数触发绘制。
|
||
if projected {
|
||
notify();
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
failure => {
|
||
// 故障画面基于上次完整发布文档,不把此次失败的半成品几何泄漏出去。
|
||
let mut document = current.document.clone();
|
||
document.frame.fault = Some(match failure {
|
||
Ok(Err(error)) => format!("Runtime background update failed: {error:#}")
|
||
.chars()
|
||
.take(1024)
|
||
.collect(),
|
||
// 内部 panic 与普通背景错误不同,必须丢弃 VM 并撤销所有原生命令,不能继续运行损坏的域。
|
||
_ => {
|
||
self.repl = None;
|
||
current.runtime_session = 0;
|
||
current.window_commands.clear();
|
||
"Runtime discarded after an internal background failure".into()
|
||
}
|
||
});
|
||
for view in document.windows.values_mut() {
|
||
view.frame.fault = document.frame.fault.clone();
|
||
}
|
||
(document, None)
|
||
}
|
||
};
|
||
// 只有提交成功才发布同世代原生命令和通知 GUI,错误/过期结果不能算一次有效刷新。
|
||
let changed = matches!(current.commit(epoch, document), Ok(Some(_)));
|
||
// 有效文档和对应命令属于同一会话,提交未接受时不能单独交付命令造成画面/原生窗口状态错配。
|
||
if changed && let Some(commands) = commands {
|
||
current.publish_window_commands(id, commands);
|
||
}
|
||
drop(current);
|
||
// changed 来自权威 commit 结果,不把失败或被替代的候选当作可见更新通知。
|
||
if changed {
|
||
notify();
|
||
}
|
||
}
|
||
|
||
/// api 线程循环收到 job 或恢复 BeforeInput 时调用;业务未执行前可等待旧刷新,执行后切换 PendingInput。
|
||
pub(super) fn begin_request(
|
||
&mut self,
|
||
job: &BuildRequest,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
) -> RequestProgress {
|
||
// 完整 PUT 独立候选,表达式隔离求值;只有 REPL/事件会进入当前持久域并等待已有刷新。
|
||
if job.kind == RequestKind::Code {
|
||
// PUT 创建隔离候选,不向当前 VM 追加语句;失败可保留旧程序的实际对象图。
|
||
return self.begin_program_request(job, namespace, notify);
|
||
}
|
||
if job.kind == RequestKind::Expression {
|
||
// evaluate 不访问活 VM/显示文档,无需等待当前只读刷新完成。
|
||
return RequestProgress::Complete(build(job, namespace, notify));
|
||
}
|
||
// 先只推进已有刷新一片,关闭和最新尺寸每个 worker 控制边界仍可见;未空闲不运行新语句。
|
||
self.refresh_viewport_mode(namespace, notify, true);
|
||
// 推进可能跨发布边界,重新读取当前接受序号,不沿用 job 作为最新状态。
|
||
let epoch = match namespace.read() {
|
||
Ok(current) => current.submission,
|
||
Err(_) => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"namespace lock is poisoned",
|
||
));
|
||
}
|
||
};
|
||
if epoch != job.submission {
|
||
// 尚未执行的旧输入被新提交撤销,不能借“仍有旧 VM”在新接受顺序后插入旧队列动作。
|
||
return RequestProgress::Complete(superseded(job.submission));
|
||
}
|
||
// 重置丢弃内存会话但保留显示文档,清帧 session/focus 使旧画面不再接受旧对象事件。
|
||
if job.kind == RequestKind::ResetRepl {
|
||
// 丢真正 VM 的内存状态,画面只作为不可交互静态值保留,不声称用户对象仍可响应。
|
||
self.repl = None;
|
||
if let Ok(mut current) = namespace.write() {
|
||
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;
|
||
}
|
||
current.runtime_session = 0;
|
||
current.window_commands.clear();
|
||
}
|
||
notify();
|
||
return RequestProgress::Complete(json(
|
||
StatusCode(200),
|
||
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()
|
||
.is_some_and(|(_, runtime)| runtime.has_pending_refresh())
|
||
{
|
||
// BeforeInput 表示尚无业务副作用,恢复可重新走 begin_request;不能把已执行输入也放此状态。
|
||
return RequestProgress::Pending(PendingRequest::BeforeInput);
|
||
}
|
||
// 事件先做 JSON 结构、kind 白名单和 Input 字段校验,再用权威发布帧核对具体资源能力。
|
||
let event = if job.kind == RequestKind::Event {
|
||
// 可信原生来源取自进程内 job.native;HTTP 正文只能被严格反序列化成普通事件。
|
||
let mut input: crate::events::Input = if let Some(input) = &job.native {
|
||
// 原生 Input 仍需下面全部字段/能力验证,可信来源只影响连续输入能否追随新布局版本。
|
||
input.clone()
|
||
} else {
|
||
// deny_unknown_fields 和类型检查由 Input 的 serde 声明执行,不按任意 JSON 字段猜动作。
|
||
match serde_json::from_str(&job.source) {
|
||
Ok(input) => input,
|
||
Err(error) => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(400),
|
||
error.to_string(),
|
||
));
|
||
}
|
||
}
|
||
};
|
||
// 先按输入种类筛选再查资源能力:合法 JSON 不代表有权触发任意名字的宿主操作。
|
||
if !input.kind.starts_with("developer-open:")
|
||
&& !matches!(
|
||
input.kind.as_str(),
|
||
"click"
|
||
| "pointermove"
|
||
| "pointerdown"
|
||
| "pointerup"
|
||
| "pointerleave"
|
||
| "pointercancel"
|
||
| "focus"
|
||
| "scroll"
|
||
| "page-pan"
|
||
| "page-zoom"
|
||
| "viewport-pan"
|
||
| "viewport-zoom"
|
||
| "scene-orbit"
|
||
| "scene-pan"
|
||
| "scene-zoom"
|
||
| "tab-activate"
|
||
| "tab-close"
|
||
| "tab-detach"
|
||
| "tab-drop"
|
||
| "window-close"
|
||
)
|
||
{
|
||
// 开发工具名字由后续工具白名单再查;其余只接受明确列出的原生输入类别。
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(400),
|
||
"unsupported event kind",
|
||
));
|
||
}
|
||
if let Err(message) = input.validate() {
|
||
// 结构错属于 400,在碰任何活 VM 对象之前回应,不能当作过期帧冲突或源运行错误。
|
||
return RequestProgress::Complete(error_response(StatusCode(400), message));
|
||
}
|
||
// 以下窗口、session、版本与几何能力都从同一个权威发布状态读锁中核对。
|
||
let current = match namespace.read() {
|
||
Ok(current) => current,
|
||
Err(_) => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"namespace lock poisoned",
|
||
));
|
||
}
|
||
};
|
||
// 原生 relay 排队期间可能又完成了 BeforeInput 刷新;在此再追随一次同目标新版本,HTTP 保持严格。
|
||
if job.native.is_some()
|
||
&& !crate::events::refresh_continuous_input(&mut input, ¤t)
|
||
{
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(409),
|
||
"native target was replaced or detached",
|
||
));
|
||
}
|
||
// 必须选输入声明的窗口帧,不能因为副窗缺失就回退到主窗口同数值 Mount。
|
||
let frame = if input.window == crate::window::MAIN_WINDOW {
|
||
¤t.document.frame
|
||
} else {
|
||
// 副窗只来自 current.document.windows,GUI 残留 OS 窗口不自动代表活域资源。
|
||
let Some(document) = current.document.windows.get(&input.window) else {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(409),
|
||
"window is no longer active",
|
||
));
|
||
};
|
||
&document.frame
|
||
};
|
||
// 窗口关闭允许处理已失活/故障副窗口,不像点击一样要求可运行 VM;由专用宿主退役路径处理。
|
||
if input.kind == "window-close"
|
||
&& frame.window == input.window
|
||
&& frame.session == input.session
|
||
{
|
||
// 有效资源身份但 VM 已停的关闭可进入纯宿主退役,其他输入仍必须拒绝。
|
||
let owns = self.repl.as_ref().is_some_and(|(owner, runtime)| {
|
||
*owner == epoch && runtime.id == input.session && input.session != 0
|
||
});
|
||
// 画面故障、无对应活 VM 或 VM 内已 faulted 都说明不能调用可覆盖的源 Window.close。
|
||
let stopped = frame.fault.is_some()
|
||
|| !owns
|
||
|| self
|
||
.repl
|
||
.as_ref()
|
||
.is_some_and(|(_, runtime)| runtime.is_faulted());
|
||
if stopped {
|
||
// close_inactive_window 需要发布写锁,调用前必须释放这里持有的读锁。
|
||
drop(current);
|
||
return RequestProgress::Complete(
|
||
self.close_inactive_window(&input, epoch, namespace, notify),
|
||
);
|
||
}
|
||
}
|
||
// 标签拖放同时验证目标窗口的会话、版本、无故障及插入位置,源窗口校验不能替代目标校验。
|
||
if let Some(target) = &input.tab_drop {
|
||
// 拖放目标同样要找到原窗口权威帧,不能只验证源标签仍存在就信任目标下标。
|
||
let target_frame = if target.window == crate::window::MAIN_WINDOW {
|
||
Some(¤t.document.frame)
|
||
} else {
|
||
current
|
||
.document
|
||
.windows
|
||
.get(&target.window)
|
||
.map(|document| &document.frame)
|
||
};
|
||
if target_frame.is_none_or(|frame| {
|
||
// 列表插入缝允许等于 tabs.len,超过长度、换代、换几何或故障都拒绝。
|
||
frame.window != target.window
|
||
|| frame.session != input.session
|
||
|| frame.layout_version != target.layout_version
|
||
|| frame.fault.is_some()
|
||
|| target.index > frame.tabs.len()
|
||
}) || self
|
||
.repl
|
||
.as_ref()
|
||
.is_none_or(|(_, runtime)| !runtime.accepts_tab_drop(&input))
|
||
{
|
||
// 错误是本次目标已过期,不让源 moveToWindow 方法运行到一半才发现无法重排。
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(409),
|
||
"drop targets an obsolete or inactive frame",
|
||
));
|
||
}
|
||
}
|
||
// 统一事件归属门:窗口/session/版本及操作对应资源必须都合法;window-close 特别允许旧布局版本。
|
||
if frame.window != input.window
|
||
|| frame.session != input.session
|
||
|| (input.kind != "window-close" && frame.layout_version != input.layout_version)
|
||
|| frame.fault.is_some()
|
||
|| if input.kind == "window-close"
|
||
|| input.kind.starts_with("developer-open:")
|
||
|| matches!(input.kind.as_str(), "page-pan" | "page-zoom")
|
||
{
|
||
// 全窗关闭、工具打开与整页相机不指向某个 Mount,必须携带零目标,防止身份多义。
|
||
input.mount != 0
|
||
} else if matches!(
|
||
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 {
|
||
// 普通 Mount 输入查出现表再按 kind 校验对应能力,不能只验证存在同号几何。
|
||
!frame.mounts.iter().any(|mount| {
|
||
mount.id == input.mount
|
||
&& if input.kind.starts_with("viewport-") {
|
||
// 二维局部相机仅允许 CanvasViewport,普通可点击矩形不能变成相机目标。
|
||
mount.canvas_viewport
|
||
} else if input.kind.starts_with("scene-") {
|
||
// 3D 手势还要求显式控制器能力,scroll 要有滚动投影,点击/焦点则要求 interactive。
|
||
mount.scene3d_viewport && mount.camera_controls
|
||
} else if input.kind == "scroll" {
|
||
// 滚动投影存在才有滚动语义,不凭 Block 名义类型或外框可点击推测。
|
||
mount.scroll.is_some()
|
||
} else {
|
||
// 点击/焦点等至少要求交互资格;具体源 hitTest/指针订阅仍在 bridge 中再查。
|
||
mount.interactive
|
||
}
|
||
})
|
||
}
|
||
|| self.repl.as_ref().is_none_or(|(context, runtime)| {
|
||
// 发布帧与实际活域必须匹配;静态保留画面不能用作对不存在 VM 的调用凭据。
|
||
*context != epoch
|
||
|| runtime.id != input.session
|
||
|| (input.kind != "window-close" && !runtime.accepts_window(input.window))
|
||
})
|
||
{
|
||
// 同一统一门在任何源回调之前拒绝,不能先执行部分动作再补查会话身份。
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(409),
|
||
"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
|
||
{
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(400),
|
||
"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
|
||
&& input.point.is_some_and(|point| {
|
||
// 先检查出现外框,再用面板/视图逆映射检测真实内容 clip,边框不作缩放锚点。
|
||
let point = crate::geometry::Point::new(point[0], point[1]);
|
||
mount.contains(point)
|
||
&& mount.viewport_clip.is_some_and(|clip| {
|
||
mount.view_point(point, true).is_some_and(|point| {
|
||
point.x >= clip[0]
|
||
&& point.y >= clip[1]
|
||
&& point.x < clip[0] + clip[2]
|
||
&& point.y < clip[1] + clip[3]
|
||
})
|
||
})
|
||
})
|
||
})
|
||
{
|
||
// 锚点必须在实际内容区域内,外框/面板反投影验证失败就先拒绝,不运行相机控制器。
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(400),
|
||
"zoom anchor is outside content viewport",
|
||
));
|
||
}
|
||
// 世界 UI 的非线性位移变换需要位置锚点;普通二维视口的线性位移可以省略 point。
|
||
if (input.kind.starts_with("viewport-") || input.kind.starts_with("scene-"))
|
||
&& input.point.is_none()
|
||
&& frame
|
||
.mounts
|
||
.iter()
|
||
.any(|mount| mount.id == input.mount && mount.panel.is_some())
|
||
{
|
||
// 缺锚点的线性二维增量尚可解释,世界面板的非线性映射则无法形成可靠局部位移。
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(400),
|
||
"world-space UI camera input requires a pointer",
|
||
));
|
||
}
|
||
Some(input)
|
||
} else {
|
||
// Console 不是事件,不制造一个空 Input;下面按 source 执行同一持久会话的交互语句。
|
||
None
|
||
};
|
||
// 只有当前 epoch 没有实际会话才惰性创建空 Console;正常输入沿用已有 VM,不为每次 POST 复制 globals。
|
||
if self
|
||
.repl
|
||
.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 => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"session identities exhausted",
|
||
));
|
||
}
|
||
};
|
||
// 空 Console 初建只沿用主壳尺寸,不从静态旧帧复制可变业务对象或副窗资源。
|
||
let viewport = namespace.read().ok().and_then(|current| current.viewport);
|
||
match endor_host::ReplSession::new_with_viewport(self.next_session, viewport) {
|
||
Ok(session) => {
|
||
// 仅共享状态仍是当前 epoch 才记录新命令世代;真正画面仍等待源程序打开页面。
|
||
if let Ok(mut current) = namespace.write()
|
||
&& current.submission == epoch
|
||
{
|
||
current.publish_window_commands(session.id, Vec::new());
|
||
}
|
||
self.repl = Some((epoch, session));
|
||
}
|
||
Err(error) => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
error.to_string(),
|
||
));
|
||
}
|
||
}
|
||
}
|
||
// 业务执行前复制合法视口集合,之后整个 VM 调用不持 namespace 锁。
|
||
let viewports = match namespace.read() {
|
||
Ok(current) => current_viewports(¤t),
|
||
Err(_) => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"namespace lock is poisoned",
|
||
));
|
||
}
|
||
};
|
||
// 惰性创建已保证 repl 存在,借用这个实际会话而非重放历史或建立每请求临时 VM。
|
||
let session = &mut self.repl.as_mut().expect("initialized REPL").1;
|
||
// 保存实际会话号,执行后可能收到新 PUT,finish_input 会再次核验归属。
|
||
let id = session.id;
|
||
// 统一收集可诊断错误与内部 panic,后者必须丢弃域,不能继续借损坏对象图交互。
|
||
let result = catch_unwind(AssertUnwindSafe(|| -> Result<_> {
|
||
// 两种输入只执行一次业务,挂起时 PendingInput 将保存这个返回值,只继续视觉阶段。
|
||
let result = if let Some(event) = &event {
|
||
session.dispatch_incremental(event, &viewports)?
|
||
} else {
|
||
session.execute_incremental(&job.source, &job.namespace, &viewports)?
|
||
};
|
||
// 业务执行可能已改 VM,但展示未完成时暂不取窗口命令;PendingInput 保存结果并只恢复展示阶段。
|
||
let commands = if session.has_pending_refresh() {
|
||
Vec::new()
|
||
} else {
|
||
session.take_window_commands()?
|
||
};
|
||
Ok((result, commands))
|
||
}));
|
||
// 是否需对可见帧加故障条取决于 VM 的实际执行状态,普通编译失败不等于整个域已损坏。
|
||
let faulted = session.is_faulted();
|
||
if session.has_pending_refresh() {
|
||
// 只有正常完成业务输入的结果可保存在 PendingInput;恢复时沿用 result,不再执行一次原始输入。
|
||
if let Ok(Ok((result, _))) = result {
|
||
return RequestProgress::Pending(PendingRequest::Input(Box::new(PendingInput {
|
||
epoch,
|
||
id,
|
||
event: event.is_some(),
|
||
result,
|
||
})));
|
||
}
|
||
// 启动失败且残留挂起刷新时直接丢弃域,不能保留一个无法安全推进的 HTTP 请求。
|
||
// 不给无法恢复的执行错误留下“等待下一片可能成功”的请求,保留完整旧画面另作响应。
|
||
self.repl = None;
|
||
}
|
||
RequestProgress::Complete(self.finish_input(
|
||
job,
|
||
namespace,
|
||
notify,
|
||
epoch,
|
||
id,
|
||
event.is_some(),
|
||
faulted,
|
||
result,
|
||
))
|
||
}
|
||
|
||
/// api 外层循环保存原 job 后每量子调用,按继续状态分流,只有 BeforeInput 能重新进入业务入口。
|
||
pub(super) fn resume_request(
|
||
&mut self,
|
||
job: &BuildRequest,
|
||
pending: PendingRequest,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
) -> RequestProgress {
|
||
match pending {
|
||
// 业务尚未执行,可重新核验提交和等待条件;Input/Candidate 都只继续原执行域。
|
||
PendingRequest::BeforeInput => self.begin_request(job, namespace, notify),
|
||
PendingRequest::Input(input) => self.resume_input(job, input, namespace, notify),
|
||
PendingRequest::Candidate(runtime) => {
|
||
self.resume_candidate(job, runtime, namespace, notify)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// resume_request 的 Input 分支调用,核验原会话并继续原视觉工作,保留先前返回值/统计不重执行语句。
|
||
fn resume_input(
|
||
&mut self,
|
||
job: &BuildRequest,
|
||
mut input: Box<PendingInput>,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
) -> RequestProgress {
|
||
// 可继续条件和最新合法尺寸在同一读锁中取得,执行时不长期持有发布锁。
|
||
let viewports = match namespace.read() {
|
||
// 正在执行输入只要仍属当前提交或仍拥有权威 VM 就可收敛;两项皆失效才彻底废弃。
|
||
Ok(current)
|
||
if current.submission == input.epoch || current.runtime_session == input.id =>
|
||
{
|
||
current_viewports(¤t)
|
||
}
|
||
Ok(_) => {
|
||
// 提交和权威 VM 都已撤销,不能把旧输入结果重新提交到另一个活动程序上。
|
||
self.repl = None;
|
||
return RequestProgress::Complete(superseded(job.submission));
|
||
}
|
||
Err(_) => {
|
||
self.repl = None;
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"namespace lock is poisoned",
|
||
));
|
||
}
|
||
};
|
||
// PendingInput 不自己强持另一个运行时,必须与 Worker 当前拥有的真实 VM 一致。
|
||
let Some((epoch, session)) = &mut self.repl else {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(409),
|
||
"input runtime was discarded",
|
||
));
|
||
};
|
||
// 接受序号与真实 VM 会话号都须匹配;同 epoch 中若换过会话,也不能把旧 PendingInput 接到新堆续跑。
|
||
if *epoch != input.epoch || session.id != input.id {
|
||
return RequestProgress::Complete(superseded(job.submission));
|
||
}
|
||
// 只推进 resize/刷新,catch_unwind 保证源视觉方法 panic 后可撤销整个损坏域。
|
||
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);
|
||
}
|
||
Ok(Some((document, session.take_window_commands()?)))
|
||
}));
|
||
// 恢复失败是否停域由实际 runtime 状态决定,交 finish_input 统一决定画面故障报告。
|
||
let faulted = session.is_faulted();
|
||
// Pending 原样保留输入元数据,Complete 才把新文档补入第一次业务执行的结果。
|
||
let result = match resumed {
|
||
Ok(Ok(None)) => return RequestProgress::Pending(PendingRequest::Input(input)),
|
||
Ok(Ok(Some((document, commands)))) => {
|
||
// 不覆盖 value/input/bindings,避免一次挂起输入变成第二次 Console 记录。
|
||
input.result.document = document;
|
||
Ok(Ok((input.result, commands)))
|
||
}
|
||
Ok(Err(error)) => Ok(Err(error)),
|
||
Err(panic) => Err(panic),
|
||
};
|
||
RequestProgress::Complete(self.finish_input(
|
||
job,
|
||
namespace,
|
||
notify,
|
||
input.epoch,
|
||
input.id,
|
||
input.event,
|
||
faulted,
|
||
result,
|
||
))
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
/// begin_request/resume_input 终结输入时调用,写锁内核验接受身份,提交完整文档与同 VM 命令后回应。
|
||
fn finish_input(
|
||
&mut self,
|
||
job: &BuildRequest,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
epoch: u64,
|
||
id: u64,
|
||
event: bool,
|
||
faulted: bool,
|
||
result: InputOutcome,
|
||
) -> HttpResponse {
|
||
// 锁覆盖提交检查/文档/命令世代更新,HTTP 新 PUT 不能插在验证与提交之间制造竞态。
|
||
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 {
|
||
// 已失去实际画面归属才彻底丢旧 VM,避免旧输入在另一代程序上产生回执或发布。
|
||
drop(current);
|
||
self.repl = None;
|
||
return superseded(job.submission);
|
||
}
|
||
// 新 PUT 撤销未执行旧队列项,但旧 VM 已开始的变更要先收敛,候选失败才有一致的旧堆可保留。
|
||
if let Some((owner, runtime)) = &mut self.repl
|
||
&& runtime.id == id
|
||
{
|
||
// 更新活 VM 的接受 epoch,而不改变其 session;不是把它冒充为新候选或复制对象图。
|
||
*owner = current.submission;
|
||
}
|
||
current.submission
|
||
} else {
|
||
// 接受序号没变,直接沿用原输入 epoch,不制造额外 revision 或 session。
|
||
epoch
|
||
};
|
||
match result {
|
||
Ok(Ok((result, commands))) => {
|
||
// 有标题/焦点等命令即使无新文档也需通知 GUI,和 rendered 的含义分别记录。
|
||
let projected = !commands.is_empty();
|
||
// rendered 只表示此次交付带新文档,成功执行但页面未变仍可返回 value/session;不因此虚增 revision。
|
||
let rendered = result.document.is_some();
|
||
if let Some(document) = result.document {
|
||
// 可见文档必须先完整验证并被当前 epoch 接受,再发布其窗口命令;不能先回成功响应后补提交。
|
||
match current.commit(epoch, document) {
|
||
Ok(Some(_)) => {}
|
||
Ok(None) => return superseded(job.submission),
|
||
Err(error) => return error_response(StatusCode(500), error.to_string()),
|
||
}
|
||
}
|
||
current.publish_window_commands(id, commands);
|
||
// 在释放锁前读取这次确定的 revision,响应不再为字符串化借用共享状态。
|
||
let revision = current.revision;
|
||
drop(current);
|
||
// 有新画面或仅有原生命令都要通知;纯值计算没有两者时不强行重绘,响应仍正常返回值。
|
||
if rendered || projected {
|
||
notify();
|
||
}
|
||
json(
|
||
StatusCode(200),
|
||
serde_json::json!({"ok":true,"execution":if event { "event" } else { "repl" },"persistent":true,"session":id,"input":result.input,"value":result.value,"bindings":result.bindings,"rendered":rendered,"revision":revision}),
|
||
)
|
||
}
|
||
Ok(Err(error)) => {
|
||
if event || faulted {
|
||
// 事件/渲染回调故障会影响整个共享 VM;单纯 Console 编译错误可只返回 422 而不覆盖画面。
|
||
let mut document = current.document.clone();
|
||
// 同 VM 保留最初故障原因,后续“域已停止”错误不能覆盖真正回调故障;其他世代旧图不沿用故障。
|
||
let fault = (document.frame.session == id)
|
||
.then(|| document.frame.fault.clone())
|
||
.flatten()
|
||
.unwrap_or_else(|| format!("{error:#}").chars().take(1024).collect());
|
||
document.frame.fault = Some(fault.clone());
|
||
for view in document.windows.values_mut() {
|
||
// 这些副窗共享已故障的持久 VM,同步显示同一原因,不能只在发生事件的一个窗口标记故障。
|
||
view.frame.fault = Some(fault.clone());
|
||
}
|
||
// 尽力提交故障条,主响应仍保留原始执行错误;不能因二次故障覆盖真正原因。
|
||
let _ = current.commit(epoch, document);
|
||
drop(current);
|
||
notify();
|
||
}
|
||
compile_error(error)
|
||
}
|
||
Err(_) => {
|
||
// Rust panic 不作为普通语言错误继续执行,清运行时/命令资格但保留上次完整静态画面。
|
||
let mut document = current.document.clone();
|
||
document.frame.fault =
|
||
Some("Runtime discarded after an internal execution failure".into());
|
||
for view in document.windows.values_mut() {
|
||
// panic 后保留最后完整画面并附统一故障条,下面会撤销运行时资格,不能继续向旧对象派发输入。
|
||
view.frame.fault = document.frame.fault.clone();
|
||
}
|
||
// 输入 panic 保留最近画面但撤销运行时/命令并发布故障条,再将 self.repl 丢弃。
|
||
current.runtime_session = 0;
|
||
current.window_commands.clear();
|
||
// 优先保留原始 panic 的响应,故障画面提交尽力而为;不因二次提交失败重新使用已损坏 VM。
|
||
let _ = current.commit(epoch, document);
|
||
drop(current);
|
||
self.repl = None;
|
||
notify();
|
||
json(
|
||
StatusCode(500),
|
||
serde_json::json!({"ok":false,"session_reset":true,"error":"Console execution failed internally; session discarded, displayed page preserved"}),
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 新提交/候选过期或失败时调用,若旧 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 身份都不改变。
|
||
*epoch = current.submission;
|
||
} else {
|
||
// 权威已经撤销或切到另一代,不能仅因还持有 Rust ReplSession 就继续执行。
|
||
self.repl = None;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// begin_request 的 Code 分支调用,先核验接受身份,再分配新 session 创建隔离候选,旧 self.repl 保持不动。
|
||
fn begin_program_request(
|
||
&mut self,
|
||
job: &BuildRequest,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
) -> RequestProgress {
|
||
// 候选仅继承主壳当前逻辑尺寸,不继承旧程序的可变对象或副窗资源。
|
||
let viewport = match namespace.read() {
|
||
Ok(current) if current.submission == job.submission => current.viewport,
|
||
Ok(current) => {
|
||
// 排队期间已有更新 PUT/DELETE,当前候选还没执行就应拒绝,不浪费新 VM 构建。
|
||
self.preserve_live_owner(¤t);
|
||
return RequestProgress::Complete(superseded(job.submission));
|
||
}
|
||
Err(_) => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"namespace lock poisoned",
|
||
));
|
||
}
|
||
};
|
||
self.next_session = match self.next_session.checked_add(1) {
|
||
Some(id) => id,
|
||
None => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"session identities exhausted",
|
||
));
|
||
}
|
||
};
|
||
// 分配过的 session 即使候选失败也不回收,避免排队旧输入命中下一次新域。
|
||
let id = self.next_session;
|
||
// 候选装载运行在独立 ReplSession,未成功前不修改 self.repl,旧程序仍是权威展示来源。
|
||
let started = catch_unwind(AssertUnwindSafe(|| {
|
||
endor_host::ReplSession::load_incremental(
|
||
&job.source,
|
||
&job.namespace,
|
||
job.submission,
|
||
id,
|
||
viewport,
|
||
)
|
||
}));
|
||
// 两层 Result 分别区分 panic 和语言/布局错误;None 文档表示保留完整候选继续视觉阶段。
|
||
let result = match started {
|
||
// 候选业务装载成功但布局未完成,整个候选移进 PendingRequest;不能把 None 当成空文档提交。
|
||
Ok(Ok((runtime, None))) => {
|
||
return RequestProgress::Pending(PendingRequest::Candidate(Box::new(runtime)));
|
||
}
|
||
Ok(Ok((runtime, Some(document)))) => match runtime.take_window_commands() {
|
||
Ok(commands) => Ok(Ok((runtime, document, commands))),
|
||
Err(error) => Ok(Err(error)),
|
||
},
|
||
Ok(Err(error)) => Ok(Err(error)),
|
||
Err(panic) => Err(panic),
|
||
};
|
||
RequestProgress::Complete(self.finish_candidate(job, namespace, notify, id, result))
|
||
}
|
||
|
||
/// resume_request 的 Candidate 分支调用,用同一个隔离 VM 继续布局/绘制,期间持续检查提交是否已过期。
|
||
fn resume_candidate(
|
||
&mut self,
|
||
job: &BuildRequest,
|
||
mut runtime: Box<endor_host::ReplSession>,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
) -> RequestProgress {
|
||
// 接受身份验证和主壳尺寸采样同锁,旧副窗口尺寸不作为新域的布局输入。
|
||
let viewports = match namespace.read() {
|
||
Ok(current) if current.submission == job.submission => {
|
||
// 候选不沿用旧副窗口的视口;副窗口资源归旧 VM,只有主壳当前尺寸可作为新候选分配条件。
|
||
current
|
||
.viewport
|
||
.map(|size| (crate::window::MAIN_WINDOW, size))
|
||
.into_iter()
|
||
.collect()
|
||
}
|
||
Ok(current) => {
|
||
self.preserve_live_owner(¤t);
|
||
return RequestProgress::Complete(superseded(job.submission));
|
||
}
|
||
Err(_) => {
|
||
return RequestProgress::Complete(error_response(
|
||
StatusCode(500),
|
||
"namespace lock poisoned",
|
||
));
|
||
}
|
||
};
|
||
// 保留候选实际 session,不能在每片恢复时重新分配 ID 或重建 UI 类型域。
|
||
let id = runtime.id;
|
||
// 续跑视觉状态,不重执行顶层语句或 main;错误/panic 都交 finish_candidate 保留旧活程序。
|
||
let resumed = catch_unwind(AssertUnwindSafe(|| -> Result<_> {
|
||
// 与活域一样,resize 可重定向外部目标或继续原 Work,但始终留在此候选 owning VM 上。
|
||
let document = runtime.resize_windows_incremental(&viewports, false)?;
|
||
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()?)))
|
||
}));
|
||
// Complete 才把持久 runtime、完整文档和命令作为一组交给切换发布,Pending 继续原 Box。
|
||
let result = match resumed {
|
||
Ok(Ok(None)) => return RequestProgress::Pending(PendingRequest::Candidate(runtime)),
|
||
Ok(Ok(Some((document, commands)))) => Ok(Ok((*runtime, document, commands))),
|
||
Ok(Err(error)) => Ok(Err(error)),
|
||
Err(panic) => Err(panic),
|
||
};
|
||
RequestProgress::Complete(self.finish_candidate(job, namespace, notify, id, result))
|
||
}
|
||
|
||
/// 完整加载/恢复候选终结时调用,写锁内二次核验 submission,只有完整成功才切换文档、命令与活 VM。
|
||
fn finish_candidate(
|
||
&mut self,
|
||
job: &BuildRequest,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
id: u64,
|
||
result: CandidateOutcome,
|
||
) -> HttpResponse {
|
||
// 字节码/源视觉执行不持锁,只有最终这个切换边界持锁原子更新外部权威。
|
||
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)
|
||
{
|
||
// 写锁内先原子提交文档,再切换命令世代与活域;解锁后唤醒 GUI,新 VM 至此才取代旧 VM。
|
||
Ok(Some(revision)) => {
|
||
current.publish_window_commands(id, commands);
|
||
self.repl = Some((job.submission, runtime));
|
||
drop(current);
|
||
notify();
|
||
json(
|
||
StatusCode(200),
|
||
serde_json::json!({"ok":true,"namespace":job.namespace,"submission":job.submission,"revision":revision,"diagnostics":[],"session":id,"persistent":true}),
|
||
)
|
||
}
|
||
Ok(None) => superseded(job.submission),
|
||
Err(error) => error_response(StatusCode(500), error.to_string()),
|
||
},
|
||
failed => {
|
||
// 候选失败只更新旧活域 epoch,保留其堆与页面;不存在旧域时也不会借用其他会话伪造成功。
|
||
self.preserve_live_owner(¤t);
|
||
drop(current);
|
||
match failed {
|
||
Ok(Err(error)) => compile_error(error),
|
||
_ => error_response(
|
||
StatusCode(500),
|
||
"candidate failed internally; previous runtime preserved",
|
||
),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// begin_request 的 Expression 分支和 API 测试一次性构建路径调用;无持久 VM 输出,源码构建前后核验接受身份。
|
||
fn build(
|
||
job: &BuildRequest,
|
||
namespace: &Arc<RwLock<NamespaceState>>,
|
||
notify: &Notify,
|
||
) -> Response<std::io::Cursor<Vec<u8>>> {
|
||
// 隔离表达式不读写持久 REPL 堆,也不发布文档,响应明确 persistent=false。
|
||
if job.kind == RequestKind::Expression {
|
||
// 表达式只回文本,不能从已有画面反取 globals,成功也明确 persistent=false。
|
||
return match catch_unwind(AssertUnwindSafe(|| endor_host::evaluate(&job.source))) {
|
||
Ok(Ok(value)) => json(
|
||
StatusCode(200),
|
||
serde_json::json!({"ok":true,"value":value,"execution":"isolated-expression","persistent":false}),
|
||
),
|
||
Ok(Err(error)) => compile_error(error),
|
||
Err(_) => error_response(StatusCode(500), "expression execution failed internally"),
|
||
};
|
||
}
|
||
// 一次性构建开始前先拒绝过期 job,节省无权发布的执行;锁外求值结束后仍须再次核验,不能只检查一次。
|
||
match namespace.read() {
|
||
Ok(current) if current.submission != job.submission => return superseded(job.submission),
|
||
Err(_) => return error_response(StatusCode(500), "namespace lock is poisoned"),
|
||
_ => {}
|
||
}
|
||
// 一次性构建同样用独立 VM,计算锁外执行,返回后再次核对 submission 才能发布原生快照。
|
||
// 包住一次性 VM 执行,panic 也不会让 HTTP 客户端一直等待失去响应的 worker。
|
||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||
endor_host::compile(&job.source, &job.namespace, job.submission)
|
||
}));
|
||
// 重新取发布写锁再检查 submission,候选计算期间新请求可接受但旧结果无权覆盖。
|
||
let Ok(mut current) = namespace.write() else {
|
||
return error_response(StatusCode(500), "namespace lock is poisoned");
|
||
};
|
||
// 这是计算后的第二次检查;期间新提交即使源码相同也拥有新 submission,旧候选不得覆盖它。
|
||
if current.submission != job.submission {
|
||
return superseded(job.submission);
|
||
}
|
||
match result {
|
||
Ok(Ok(document)) => match current.commit(job.submission, document) {
|
||
Ok(Some(revision)) => {
|
||
drop(current);
|
||
notify();
|
||
json(
|
||
StatusCode(200),
|
||
serde_json::json!({ "ok": true, "namespace": job.namespace,
|
||
"submission": job.submission, "revision": revision, "diagnostics": [] }),
|
||
)
|
||
}
|
||
Ok(None) => superseded(job.submission),
|
||
Err(error) => error_response(StatusCode(500), error.to_string()),
|
||
},
|
||
Ok(Err(error)) => compile_error(error),
|
||
Err(_) => error_response(
|
||
StatusCode(500),
|
||
"candidate execution failed internally; previous document preserved",
|
||
),
|
||
}
|
||
}
|