app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
//! Progress blocks for slow verbs.
//!
//! Two halves of one contract:
//! * a slow verb's own response ends with a TERMINAL frame — `percent:100`
//! plus the real `elapsedSec`, so the caller learns what the operation
//! actually costs on this machine;
//! * `swx_progress` returns `active[]` — the live frames of everything in
//! flight — so an AI (or a UI) can poll ~1/s and drive a bar while a
//! 90-second interference check runs.
//!
//! A job registers on creation and deregisters on drop, so an early return or a
//! panic can never leave a phantom job pinned in `active[]`.
use serde_json::{json, Value};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
struct Entry {
phase: &'static str,
step_label: String,
estimated_sec: u64,
started: Instant,
/// What the verb has done so far, for `swx_progress` (0.9.12): a sweep's
/// stage and its counts.
detail: Value,
}
#[derive(Default)]
pub struct Jobs {
seq: AtomicU64,
live: Mutex<Vec<(u64, Entry)>>,
}
impl Jobs {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
/// Register a job for the life of the returned guard.
pub fn start(
self: &Arc<Self>,
phase: &'static str,
step_label: impl Into<String>,
estimated_sec: u64,
) -> JobGuard {
let id = self.seq.fetch_add(1, Ordering::SeqCst) + 1;
let started = Instant::now();
let entry = Entry { phase, step_label: step_label.into(), estimated_sec, started, detail: Value::Null };
self.live.lock().unwrap_or_else(|e| e.into_inner()).push((id, entry));
JobGuard { jobs: Arc::clone(self), id, started }
}
/// Live frames for `swx_progress`.
pub fn active(&self) -> Vec<Value> {
let live = self.live.lock().unwrap_or_else(|e| e.into_inner());
live.iter()
.map(|(_, e)| {
let elapsed = round1(e.started.elapsed().as_secs_f64());
json!({
"phase": e.phase,
"stepLabel": e.step_label,
"percent": Value::Null,
"elapsedSec": elapsed,
"estimatedSec": e.estimated_sec,
"etaSec": round1((e.estimated_sec as f64 - elapsed).max(0.0)),
"confidence": "estimate",
"startKind": "warm",
"detail": e.detail,
})
})
.collect()
}
}
pub struct JobGuard {
jobs: Arc<Jobs>,
id: u64,
started: Instant,
}
impl JobGuard {
/// Say what the verb is doing now — shown by `swx_progress` while it runs.
pub fn update(&self, step_label: impl Into<String>, detail: Value) {
let mut live = self.jobs.live.lock().unwrap_or_else(|e| e.into_inner());
if let Some((_, e)) = live.iter_mut().find(|(id, _)| *id == self.id) {
e.step_label = step_label.into();
e.detail = detail;
}
}
/// The terminal frame that rides on the verb's own response.
pub fn finished(&self) -> Value {
let live = self.jobs.live.lock().unwrap_or_else(|e| e.into_inner());
let (phase, label, est) = live
.iter()
.find(|(id, _)| *id == self.id)
.map(|(_, e)| (e.phase, e.step_label.clone(), e.estimated_sec))
.unwrap_or(("unknown", String::new(), 0));
json!({
"phase": phase,
"stepLabel": label,
"percent": 100,
"elapsedSec": round1(self.started.elapsed().as_secs_f64()),
"estimatedSec": est,
"etaSec": 0,
"confidence": "estimate",
"startKind": "warm",
})
}
}
impl Drop for JobGuard {
fn drop(&mut self) {
let mut live = self.jobs.live.lock().unwrap_or_else(|e| e.into_inner());
live.retain(|(id, _)| *id != self.id);
}
}
fn round1(v: f64) -> f64 {
(v * 10.0).round() / 10.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_job_is_active_only_while_it_lives() {
let jobs = Jobs::new();
assert!(jobs.active().is_empty());
{
let g = jobs.start("export", "Exporting STEP", 45);
let active = jobs.active();
assert_eq!(active.len(), 1);
assert_eq!(active[0]["phase"], "export");
assert_eq!(active[0]["percent"], Value::Null);
assert_eq!(g.finished()["percent"], 100);
assert_eq!(g.finished()["etaSec"], 0);
}
assert!(jobs.active().is_empty(), "the guard must deregister on drop");
}
#[test]
fn finished_reports_the_label_and_estimate() {
let jobs = Jobs::new();
let g = jobs.start("bom", "Walking assembly components", 60);
let f = g.finished();
assert_eq!(f["stepLabel"], "Walking assembly components");
assert_eq!(f["estimatedSec"], 60);
}
}