1use crate::automation::command::{lookup, Ctx, Envelope, Handler, Notice, NoticeKind, Outcome, Phase, Reply};
22use crate::automation::pointer::Pointer;
23use std::collections::VecDeque;
24use std::sync::mpsc::{channel, Receiver, Sender};
25use std::sync::{Arc, Mutex};
26
27pub struct Pending {
28 pub envelope: Envelope,
29 pub reply: Sender<Reply>,
30 deadline: Option<std::time::Instant>,
31}
32
33impl Pending {
34 fn expired(&self, frame: u64) -> bool {
35 if self.deadline.is_some_and(|d| std::time::Instant::now() >= d) {
36 let _ = self.reply.send(Reply::err(self.envelope.id, frame,
37 "host_deadline_expired: command expired before dispatch; no effects", vec![]));
38 true
39 } else { false }
40 }
41}
42
43struct Awaiting {
44 token: u64,
45 region: crate::automation::cmd_capture::Region,
46 document: Option<String>,
47 pending: Pending,
48}
49
50struct Polling {
51 pending: Pending,
52 phase: Phase,
53 poller: crate::automation::command::Poller,
54 bound: Option<String>,
58}
59
60#[derive(Default)]
61struct Inner {
62 pending: VecDeque<Pending>,
63 awaiting: Vec<Awaiting>,
64 polling: Vec<Polling>,
65 notices: Vec<Notice>,
66 notices_replied: usize,
69 pointer: Pointer,
70 next_token: u64,
71 frame: u64,
72 poisoned: Option<String>,
73 view: Option<(String, u64)>,
76}
77
78const MAX_NOTICES: usize = 256;
83
84impl Inner {
85 fn reply_notices(&mut self) -> Vec<Notice> {
97 let from = self.notices_replied.min(self.notices.len());
98 self.notices_replied = self.notices.len();
99 self.notices[from..].to_vec()
100 }
101}
102
103pub type Waker = Arc<dyn Fn() + Send + Sync>;
105
106pub const FRAME_BOUND: &[&str] = &[
112 "screenshot",
113 "illustration_presentation",
114 "state_list",
115 "state_get",
116 "state_schema",
117 "hit_rects",
118 "hit_key_docs",
119 "describe_panes",
120 "perf_stats",
121 "perf_spin",
122 "perf_reset",
123];
124
125pub fn frameless(spec: &crate::automation::command::CommandSpec) -> bool {
131 match spec.handler {
132 Handler::App(_) => spec.phase != Phase::Input && !FRAME_BOUND.contains(&spec.name),
133 Handler::Input(_) => spec.phase == Phase::Input && spec.annotations.read_only,
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum Withdrawn {
141 NotDispatched,
143 Accepted,
146}
147
148#[derive(Default)]
149pub struct AutomationQueue {
150 inner: Mutex<Inner>,
151 ctx: Mutex<Option<egui::Context>>,
152 waker: Mutex<Option<Waker>>,
153}
154
155impl AutomationQueue {
156 pub fn new() -> Arc<Self> {
157 Arc::new(Self::default())
158 }
159
160 pub fn attach(&self, ctx: &egui::Context) {
162 *self.ctx.lock().unwrap() = Some(ctx.clone());
163 if self.has_pending() { ctx.request_repaint(); }
164 }
165
166 pub fn set_waker(&self, waker: Waker) {
169 *self.waker.lock().unwrap() = Some(waker.clone());
170 if self.has_pending() { waker(); }
171 }
172
173 fn wake(&self) {
174 let waker = self.waker.lock().unwrap().clone();
175 if let Some(waker) = waker { waker(); }
176 if let Some(ctx) = self.ctx.lock().unwrap().as_ref() {
177 ctx.request_repaint();
178 }
179 }
180
181 pub fn submit(&self, envelope: Envelope) -> Receiver<Reply> {
185 self.submit_before(envelope, None)
186 }
187
188 pub fn submit_before(&self, envelope: Envelope, deadline: Option<std::time::Instant>) -> Receiver<Reply> {
190 let (tx, rx) = channel();
191 let mut inner = self.inner.lock().unwrap();
192 if let Some(why) = &inner.poisoned {
193 let _ = tx.send(Reply::err(envelope.id, inner.frame, format!("session poisoned: {why}"), vec![]));
194 return rx;
195 }
196 if lookup(&envelope.cmd).is_none() {
197 let _ = tx.send(Reply::err(envelope.id, inner.frame, format!("unknown command `{}`", envelope.cmd), vec![]));
198 return rx;
199 }
200 inner.pending.push_back(Pending { envelope, reply: tx, deadline });
201 drop(inner);
204 self.wake();
205 rx
206 }
207
208 pub fn withdraw(&self, id: u64) -> Withdrawn {
211 let mut inner = self.inner.lock().unwrap();
212 let frame = inner.frame;
213 if let Some(index) = inner.pending.iter().position(|p| p.envelope.id == id) {
214 let p = inner.pending.remove(index).expect("index from position");
215 let _ = p.reply.send(Reply::err(id, frame, "host_deadline_expired: command expired before dispatch; no effects", vec![]));
216 return Withdrawn::NotDispatched;
217 }
218 Withdrawn::Accepted
219 }
220
221 pub fn has_frameless_work(&self) -> bool {
224 let inner = self.inner.lock().unwrap();
225 !inner.polling.is_empty()
226 || inner.pending.front().is_some_and(|p| lookup(&p.envelope.cmd).is_some_and(frameless))
227 }
228
229 pub fn push_notice(&self, kind: NoticeKind, text: impl Into<String>) {
233 let mut inner = self.inner.lock().unwrap();
234 let frame = inner.frame;
235 inner.notices.push(Notice { kind, frame, text: text.into() });
236 let overflow = inner.notices.len().saturating_sub(MAX_NOTICES);
237 if overflow > 0 {
238 inner.notices.drain(0..overflow);
239 inner.notices_replied = inner.notices_replied.saturating_sub(overflow);
240 }
241 }
242
243 pub fn take_notices(&self) -> Vec<Notice> {
247 let mut inner = self.inner.lock().unwrap();
248 inner.notices_replied = 0;
249 std::mem::take(&mut inner.notices)
250 }
251
252 pub fn pointer(&self) -> Pointer {
253 self.inner.lock().unwrap().pointer.clone()
254 }
255
256 pub fn frame(&self) -> u64 {
257 self.inner.lock().unwrap().frame
258 }
259
260 pub fn has_pending(&self) -> bool {
261 let inner = self.inner.lock().unwrap();
262 !inner.pending.is_empty() || !inner.awaiting.is_empty() || !inner.polling.is_empty()
263 }
264
265 pub fn is_polling(&self, id: u64) -> bool {
269 self.inner.lock().unwrap().polling.iter().any(|p| p.pending.envelope.id == id)
270 }
271
272 pub fn poison(&self, why: impl Into<String>) {
275 let why = why.into();
276 let mut inner = self.inner.lock().unwrap();
277 inner.poisoned = Some(why.clone());
278 let frame = inner.frame;
279 for p in inner.pending.drain(..) {
280 let _ = p.reply.send(Reply::err(p.envelope.id, frame, format!("session poisoned: {why}"), vec![]));
281 }
282 for a in inner.awaiting.drain(..) {
283 let _ = a.pending.reply.send(Reply::err(a.pending.envelope.id, frame, format!("session poisoned: {why}"), vec![]));
284 }
285 }
286
287 pub fn is_poisoned(&self) -> bool {
288 self.inner.lock().unwrap().poisoned.is_some()
289 }
290
291 pub fn drain_input(&self, raw: &mut egui::RawInput, frame: u64, ppp: f32, view: Option<egui::Rect>, ctx: &egui::Context) {
298 let mut inner = self.inner.lock().unwrap();
299 inner.frame = frame;
300 inner.pointer.now = raw.time.unwrap_or_else(|| ctx.input(|i| i.time) + f64::from(raw.predicted_dt));
301
302 if !inner.awaiting.is_empty() {
304 let mut done = Vec::new();
305 for event in &raw.events {
306 if let egui::Event::Screenshot { user_data, image, .. } = event {
307 let token = user_data.data.as_ref().and_then(|d| d.downcast_ref::<u64>()).copied();
308 if let Some(token) = token {
309 if let Some(pos) = inner.awaiting.iter().position(|a| a.token == token) {
310 let a = inner.awaiting.remove(pos);
311 done.push((a, image.clone()));
312 }
313 }
314 }
315 }
316 for (a, image) in done {
317 let notices = inner.reply_notices();
318 let shown = inner.view.clone();
319 let switched = match (&a.document, &shown) {
323 (Some(asked), Some((drawn, _))) => asked != drawn,
324 _ => false,
325 };
326 let result = if switched {
327 Err("document_not_active: the visible document changed between the request and the capture".to_string())
328 } else {
329 crate::automation::cmd_capture::encode_capture(&image, ppp, view, &a.region).map(|(mut json, png)| {
330 if let Some((handle, revision)) = &shown {
331 json["document"] = serde_json::json!({ "documentId": handle, "revision": revision.to_string() });
332 }
333 (json, png)
334 })
335 };
336 let reply = match result {
337 Ok((json, png)) => {
338 let mut r = Reply::ok(a.pending.envelope.id, frame, json, notices);
339 r.blob = Some(png);
340 r
341 }
342 Err(e) => Reply::err(a.pending.envelope.id, frame, e, notices),
343 };
344 let _ = a.pending.reply.send(reply);
345 }
346 }
347
348 let mut keep = VecDeque::new();
350 let mut events = Vec::new();
351 while let Some(p) = inner.pending.pop_front() {
352 if p.expired(frame) { continue; }
353 let Some(spec) = lookup(&p.envelope.cmd) else { continue };
354 match (&spec.handler, spec.phase) {
355 (Handler::Input(f), Phase::Input) => {
356 let result = f(&mut inner.pointer, p.envelope.args.clone(), &mut events);
357 let notices = inner.reply_notices();
358 let reply = match result {
359 Ok(v) => Reply::ok(p.envelope.id, frame, v, notices),
360 Err(e) => Reply::err(p.envelope.id, frame, e, notices),
361 };
362 let _ = p.reply.send(reply);
363 }
364 _ => keep.push_back(p),
365 }
366 }
367 inner.pending = keep;
368 if std::mem::take(&mut inner.pointer.reset_clicks) {
372 ctx.input_mut(|i| i.pointer = egui::PointerState::default());
373 }
374 raw.modifiers = inner.pointer.modifiers.egui();
375 raw.events.extend(events);
376 }
377
378 pub fn drain_app(&self, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) {
381 let batch: Vec<Pending> = {
382 let mut inner = self.inner.lock().unwrap();
383 inner.frame = frame;
384 let mut keep = VecDeque::new();
385 let mut batch = Vec::new();
386 while let Some(p) = inner.pending.pop_front() {
387 match lookup(&p.envelope.cmd) {
388 Some(spec) if spec.phase == phase => batch.push(p),
389 _ => keep.push_back(p),
390 }
391 }
392 inner.pending = keep;
393 batch
394 };
395 for p in batch {
396 self.dispatch(p, phase, ctx, frame);
397 }
398 if self.poll(phase, ctx, frame) {
399 ctx.egui.request_repaint();
400 }
401 }
402
403 pub fn drain_frameless(&self, ctx: &mut Ctx<'_>) -> bool {
409 let frame = self.frame();
410 loop {
411 let next = {
412 let mut inner = self.inner.lock().unwrap();
413 match inner.pending.front().and_then(|p| lookup(&p.envelope.cmd)) {
414 Some(spec) if frameless(spec) => {
415 let p = inner.pending.pop_front().expect("front exists");
416 if let Handler::Input(f) = &spec.handler {
417 if !p.expired(frame) {
420 let mut events = Vec::new();
421 let result = f(&mut inner.pointer, p.envelope.args.clone(), &mut events);
422 debug_assert!(events.is_empty(), "{} is read-only but emitted input", spec.name);
423 let notices = inner.reply_notices();
424 let reply = match result {
425 Ok(v) => Reply::ok(p.envelope.id, frame, v, notices),
426 Err(e) => Reply::err(p.envelope.id, frame, e, notices),
427 };
428 let _ = p.reply.send(reply);
429 }
430 continue;
431 }
432 Some((p, spec.phase))
433 }
434 _ => None,
435 }
436 };
437 let Some((p, phase)) = next else { break };
438 self.dispatch(p, phase, ctx, frame);
439 }
440 let mutate = self.poll(Phase::Mutate, ctx, frame);
441 let read = self.poll(Phase::Read, ctx, frame);
442 mutate || read
443 }
444
445 fn dispatch(&self, p: Pending, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) {
447 if p.expired(frame) { return; }
448 let spec = lookup(&p.envelope.cmd).expect("checked at submit");
449 let Handler::App(f) = &spec.handler else { return };
450 let (args, bound) = match bind_context(&ctx.app.docs, phase, &p.envelope.cmd, p.envelope.args.clone()) {
454 Ok(bound) => bound,
455 Err(e) => {
456 let mut inner = self.inner.lock().unwrap();
457 let notices = inner.reply_notices();
458 let _ = p.reply.send(Reply::err(p.envelope.id, frame, e, notices));
459 return;
460 }
461 };
462 let before = refused_counts(&ctx.app.docs);
463 let route = bound.as_ref().and_then(|b| b.route).map(|index| ctx.app.docs.route_begin(index));
464 let result = f(ctx, args);
465 if let Some(visible) = route {
466 ctx.app.docs.route_end(visible);
467 }
468 let result = refused_by_lock(&ctx.app.docs, &before, result);
469 let handle = bound.map(|b| b.handle);
470 let result = match &handle {
471 Some(handle) => tag_context(&ctx.app.docs, handle, result),
472 None => result,
473 };
474 let mut inner = self.inner.lock().unwrap();
475 answer(&mut inner, p, phase, frame, result, handle);
476 }
477
478 fn poll(&self, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) -> bool {
481 let polling: Vec<Polling> = {
482 let mut inner = self.inner.lock().unwrap();
483 let (mine, rest) = std::mem::take(&mut inner.polling).into_iter().partition(|p| p.phase == phase);
484 inner.polling = rest;
485 mine
486 };
487 let mut still = false;
488 for mut entry in polling {
489 match (entry.poller)(ctx) {
490 None => {
491 still = true;
492 self.inner.lock().unwrap().polling.push(entry);
493 }
494 Some(result) => {
495 let result = match &entry.bound {
496 Some(handle) => tag_context(&ctx.app.docs, handle, result),
497 None => result,
498 };
499 let mut inner = self.inner.lock().unwrap();
500 answer(&mut inner, entry.pending, phase, frame, result, entry.bound);
501 still |= !inner.polling.is_empty();
502 }
503 }
504 }
505 still
506 }
507
508 pub fn publish_view(&self, handle: &str, revision: u64) {
511 let mut inner = self.inner.lock().unwrap();
512 if inner.view.as_ref().is_none_or(|(h, r)| h != handle || *r != revision) {
513 inner.view = Some((handle.to_string(), revision));
514 }
515 }
516
517 pub fn next_token(&self) -> u64 {
519 let mut inner = self.inner.lock().unwrap();
520 inner.next_token += 1;
521 inner.next_token
522 }
523}
524
525#[derive(serde::Deserialize)]
530#[serde(deny_unknown_fields, rename_all = "camelCase")]
531struct DocumentContext {
532 document_id: String,
533 expected_revision: String,
534}
535
536pub const ENGINE_ONLY: &[&str] = &[
542 "feature_stage",
543 "feature_stage_result",
544 "run_cancel",
545 "history_group_begin",
546 "history_group_end",
547 "feature_add_many",
548 "doc_import",
549 "doc_export",
550 "model_inspect",
551 "mass_properties",
552 "history_listing",
553 "frame_info",
554 "metadata_set",
555 "metadata_set_many",
556 "metadata_remove",
557];
558
559#[derive(Debug)]
562struct Bound {
563 handle: String,
564 route: Option<usize>,
565}
566
567fn bind_context(
578 docs: &crate::document::Documents,
579 phase: Phase,
580 cmd: &str,
581 mut args: serde_json::Value,
582) -> Result<(serde_json::Value, Option<Bound>), String> {
583 let Some(raw) = args.as_object_mut().and_then(|o| o.remove("context")) else {
584 return Ok((args, None));
585 };
586 let context: DocumentContext = serde_json::from_value(raw)
588 .map_err(|e| format!("invalid_arguments: context must be {{documentId, expectedRevision}}: {e}"))?;
589 let expected = parse_revision(&context.expected_revision)?;
590 match crate::handles::parse("doc", &context.document_id) {
591 crate::handles::Parsed::Current => {}
592 crate::handles::Parsed::OtherEpoch => {
593 return Err("handle_epoch_mismatch: the document handle is from another run of the app".into())
594 }
595 crate::handles::Parsed::Malformed => return Err("invalid_arguments: documentId is not a document handle".into()),
596 }
597 let Some(index) = docs.index_of_handle(&context.document_id) else {
598 return Err("document_unknown: no open document has that handle (closed, or never opened)".into());
599 };
600 if docs.work_part_in_place() {
601 return Err("document_busy: a Work part is edited in place; a document command would write the part".into());
602 }
603 let route = if index == docs.active_index() {
604 None
605 } else if ENGINE_ONLY.contains(&cmd) {
606 Some(index)
607 } else {
608 return Err("document_not_active: this command acts on the visible document; activate the named one first".into());
609 };
610 let current = docs.get(index).map_or(0, |d| d.committed_revision());
611 if phase == Phase::Mutate && current != expected {
612 return Err(format!(
613 "revision_conflict: the document is at revision {current}, not {expected}; read it again before writing"
614 ));
615 }
616 Ok((args, Some(Bound { handle: context.document_id, route })))
617}
618
619fn parse_revision(text: &str) -> Result<u64, String> {
621 let canonical = text == "0" || (!text.is_empty() && !text.starts_with('0') && text.bytes().all(|b| b.is_ascii_digit()));
622 text.parse::<u64>()
623 .ok()
624 .filter(|_| canonical)
625 .ok_or_else(|| "invalid_arguments: expectedRevision must be a canonical unsigned decimal string".into())
626}
627
628fn tag_context(
634 docs: &crate::document::Documents,
635 handle: &str,
636 result: Result<Outcome, String>,
637) -> Result<Outcome, String> {
638 let context = docs.index_of_handle(handle).and_then(|i| docs.get(i)).map(|doc| {
639 serde_json::json!({ "documentId": handle, "expectedRevision": doc.committed_revision().to_string() })
640 });
641 let tag = |v: &mut serde_json::Value| {
642 if let (Some(object), Some(context)) = (v.as_object_mut(), context.clone()) {
643 object.insert("context".into(), context);
644 }
645 };
646 match result {
647 Ok(Outcome::Done(mut v)) => {
648 tag(&mut v);
649 Ok(Outcome::Done(v))
650 }
651 Ok(Outcome::Blob { mut json, bytes }) => {
652 tag(&mut json);
653 Ok(Outcome::Blob { json, bytes })
654 }
655 other => other,
656 }
657}
658
659fn answer(inner: &mut Inner, p: Pending, phase: Phase, frame: u64, result: Result<Outcome, String>, bound: Option<String>) {
661 match result {
662 Ok(Outcome::Done(v)) => {
663 let notices = inner.reply_notices();
664 let _ = p.reply.send(Reply::ok(p.envelope.id, frame, v, notices));
665 }
666 Ok(Outcome::Blob { json, bytes }) => {
667 let notices = inner.reply_notices();
668 let mut r = Reply::ok(p.envelope.id, frame, json, notices);
669 r.blob = Some(bytes);
670 let _ = p.reply.send(r);
671 }
672 Ok(Outcome::AwaitScreenshot { token, region, document }) => {
675 inner.awaiting.push(Awaiting { token, region, document, pending: p });
676 }
677 Ok(Outcome::Poll(poller)) => inner.polling.push(Polling { pending: p, phase, poller, bound }),
678 Err(e) => {
679 let notices = inner.reply_notices();
680 let _ = p.reply.send(Reply::err(p.envelope.id, frame, e, notices));
681 }
682 }
683}
684
685fn refused_counts(docs: &crate::document::Documents) -> Vec<(u64, u64)> {
687 docs.iter().map(|d| (d.id(), d.engine.history.refused_user_edits())).collect()
688}
689
690fn refused_by_lock(
696 docs: &crate::document::Documents,
697 before: &[(u64, u64)],
698 result: Result<Outcome, String>,
699) -> Result<Outcome, String> {
700 if result.is_err() {
701 return result;
702 }
703 for doc in docs.iter() {
704 let was = before.iter().find(|(id, _)| *id == doc.id()).map(|(_, n)| *n);
705 if was.is_some_and(|was| doc.engine.history.refused_user_edits() > was) {
706 if let crate::document::Access::ReadOnly { reason } = doc.access() {
707 return Err(format!("{} is read-only: {reason}", doc.title()));
708 }
709 }
710 }
711 result
712}
713