Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
The busy gate waits out the bridge's own quick reads instead of refusing the next verb #23
fix/busy-gate-short-reads → masterWhat this fixes
With many documents open, the bridge refused a verb because of its own document snapshot.
- The snapshot. After every window-raising verb (
run_modeling_scriptincluded), the window etiquette snapshots the open documents withdocument_info. It waits only 3 s for the answer (etiquette.rs,quick). - The overrun. With about 90 documents open, that
document_infocan run past the 3 s. The exe returns, Fusion keeps working on it, and the caller's next verb arrives while it is still running. - The refusal. The busy gate refuses anything that has been running for 2 s or more, so that next verb gets
main_thread_busyfor work the bridge started itself.
Evidence from the assembly campaign on Drew's laptop (94 documents open), two consecutive calls:
0448 fusion_run_modeling_script ok _timing: pre 0.08 s, add-in 0.97 s, post 4.90 s
0449 fusion_mechanical {op: inspect} main_thread_busy busyCommand: document_info, elapsedSeconds: 4.9
It happened once in about 450 calls. It depends on load: on a quiet Fusion with the same 93 documents, the snapshot took about 1 s and I could not reproduce it on demand. A strict harness (Astra's campaign) stops on it.
What changed
busy_gate (addin.rs) now waits out the bridge's own quick reads, document_info and get_app_state:
- It re-probes
/statusevery 250 ms, for up to 15 s, before judging the add-in. - Real work in front of a verb is still refused exactly as before: a script, an import, a cloud walk, or a quick read that is still running after 15 s.
- Unit test:
the_gate_waits_out_the_bridges_own_quick_reads.
Verified
tools/rust-ship.shis green: 140 Rust tests, the Windows build and the guards. The Python suites pass.- Not reproduced live, for the load-dependence reason above.
Two things on master for you, Colby (not in this PR)
- The vendored engine is missing a directory.
rust/vendor/adom-eda-checks/src/writers/was not pushed:lib.rsdeclarespub mod writers;and master 1.13.2 does not build from source without it. I copied it fromadom/eda-bridge-standardat e8a33cc (the commit VENDOR.md names) to build locally. Everything else in the vendored copy matches that commit. rust/Cargo.lockon master is stale. It still says adom-eda-checks 0.1.4 and the bridge 1.13.0, without the writers' dependencies (roxmltree, svgtypes).
FYI for the 1.13.0 changelog: joint_origin_* and motion_link_* have now run live. All 15 assembly campaign groups pass on the dev pin: nested_context, origins, redefinition, links_two, link_single and asbuilt added 133 checks, with no failures. So the "not yet run live" line can go.
Diff Skip to comments
CHANGELOG.md+8@@ -3,6 +3,14 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.13.3 (unreleased)++- The busy gate waits out the bridge's own quick reads (`document_info`, `get_app_state`) for up to 15 s instead+ of refusing the next verb. With about 90 documents open, the post-verb document snapshot can outlive the 3 s+ the window etiquette waits for it, and the caller's next verb then found it still running and got+ `main_thread_busy` (a mechanical `inspect` in the assembly campaign, behind a 4.9 s `document_info`). Real work+ in front of a verb is refused as before.+ ## 1.13.2 (2026-10-28, insiders) The standard's shared writers, for files every bridge writes the same way.
rust/bridge/src/addin.rs+27−1@@ -167,6 +167,18 @@ pub fn loaded_evidence(pids: &[u32]) -> Option<Value> { loaded_evidence_from(None, Some(ago), port_refused()) } +/// The bridge's own quick reads. With ~90 documents open a document_info takes ~5 s, longer than+/// the 3 s the post-verb document snapshot waits, so it is still running when the caller's next+/// verb lands, and that verb was refused as main_thread_busy (2026-10-28, a mechanical inspect in+/// the assembly campaign). Such a read ends in seconds: the gate waits it out instead.+const SHORT_READS: &[&str] = &["document_info", "get_app_state"];+const SHORT_READ_WAIT: Duration = Duration::from_secs(15);++/// Is the add-in busy with one of the bridge's own quick reads, which the gate should wait out?+fn busy_with_short_read(st: &Value) -> bool {+ st["busy"].as_bool() == Some(true) && SHORT_READS.contains(&st["busyCommand"].as_str().unwrap_or(""))+}+ /// The two busy gates, in server.py's order: our own long command, then the add-in's /status. fn busy_gate(cmd: &str) -> Option<Value> { if LONG_RUNNING.contains(&cmd) || BUSY_EXEMPT.contains(&cmd) { return None; }@@ -183,7 +195,12 @@ fn busy_gate(cmd: &str) -> Option<Value> { "errorCode": "main_thread_busy", "busyCommand": bc, "elapsedSeconds": elapsed, "progress": progress, "_hint": format!("A cloud search ({bc}) is in progress{}, running for {elapsed}s. Do NOT retry add-in commands (get_app_state, document_info, etc.) — they will all be rejected until the search finishes. Commands that still work right now: fusion_window_info, fusion_screenshot_fusion, fusion_click_fusion, fusion_send_key, fusion_close_window. Wait for the search to complete, then retry.", pct.map(|p| format!(" (~{p}% done)")).unwrap_or_default())})); }- if let Some(st) = addin_status_probe(Duration::from_secs(3)) {+ if let Some(mut st) = addin_status_probe(Duration::from_secs(3)) {+ let deadline = Instant::now() + SHORT_READ_WAIT;+ while busy_with_short_read(&st) && Instant::now() < deadline {+ std::thread::sleep(Duration::from_millis(250));+ match addin_status_probe(Duration::from_secs(3)) { Some(next) => st = next, None => break }+ } // the palette (activity_banner) repaints in a fraction of a second and is advisory: a verb // that lands while it paints waits its turn in the add-in instead of being refused // a sub-second probe from another caller (the dashboard polls get_app_state every half@@ -471,6 +488,15 @@ mod tests { assert!(loaded_evidence_from(None, None, false).is_none()); } #[test]+ fn the_gate_waits_out_the_bridges_own_quick_reads() {+ // 2026-10-28: the post-verb document_info ran ~5 s with 94 documents open and the next+ // verb was refused; a quick read is waited out, real work still is not+ assert!(busy_with_short_read(&json!({"busy": true, "busyCommand": "document_info", "elapsedSeconds": 4.9})));+ assert!(busy_with_short_read(&json!({"busy": true, "busyCommand": "get_app_state", "elapsedSeconds": 2.5})));+ assert!(!busy_with_short_read(&json!({"busy": true, "busyCommand": "run_modeling_script", "elapsedSeconds": 4.9})));+ assert!(!busy_with_short_read(&json!({"busy": false, "busyCommand": "document_info"})));+ }+ #[test] fn requests_leave_the_in_flight_list_however_they_end() { assert!(IN_FLIGHT.lock().unwrap().iter().all(|(_, c, _)| c != "slow_one")); let a = InFlight::begin("slow_one");
Reviews
Comments
Log in to comment.
No comments yet.