app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
Drew Owens
0.9.0: bridge data moved out of the install cache (recovery survives updates), closed-in-place records, gates route thr…
5d02ca9
20d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
//! SOLIDWORKS' WINDOWS, seen through ab's generic desktop verbs.
//!
//! This bridge deliberately owns no window code. Screenshots, window
//! enumeration, taskbar overlays and WM_CLOSE are ab-core capabilities that
//! every bridge would otherwise reimplement (badly, and differently), so the
//! `swx_*` window verbs are thin, honest delegations: we supply the SOLIDWORKS
//! knowledge (which HWND is the app, which dialog titles are blocking, what is
//! safe to touch) and ab supplies the mechanism.
//!
//! A callback that fails NEVER fails the verb: ab being unreachable means a
//! partial answer, not a broken bridge. Every helper here degrades to "unknown"
//! and says so.
use serde_json::{json, Value};
use std::time::Duration;
/// The Win32 dialog window class. Every SOLIDWORKS modal — Save changes?,
/// Document Recovery, the export option sheets — is one of these.
pub const DIALOG_CLASS: &str = "#32770";
/// Is this a top-level application FRAME rather than a dialog?
///
/// It matters because closing a frame is not "dismissing a dialog", it is
/// QUITTING THE APPLICATION — with Save-changes modals on the way out, and on a
/// Connected edition this bridge cannot start it again.
///
/// The hole this closes: `ownerConfirmed` is true precisely when a window is
/// most provably SOLIDWORKS', and the main frame is the most provable window
/// there is. So the "not a general window killer" guard passed on the one window
/// it should never have passed on — and `swx_status` / `swx_window_info` hand
/// `mainHwnd` to callers, so it was the obvious thing to try.
pub fn is_frame_window(w: &Value, main_hwnd: Option<i64>) -> bool {
let hwnd = w.get("hwnd").and_then(|h| h.as_i64());
if hwnd.is_some() && hwnd == main_hwnd {
return true;
}
let class = w.get("className").and_then(|c| c.as_str()).unwrap_or("");
// MFC frame windows (which is what SOLIDWORKS' own windows are) register as
// `Afx:<hex>:...`. A dialog is `#32770`; ANYTHING ELSE — including a window
// ab reports with no className at all — is treated as a frame.
//
// The `!class.is_empty() &&` that used to lead this expression inverted its
// own fail-safe: a window whose class we could not read was declared "not a
// frame" and therefore closable. Same rule as `kill_is_unsafe`: an
// unanswerable question is not permission.
class != DIALOG_CLASS
}
/// Dialog kinds this bridge itself causes. REPORTED, not licensed: closing even
/// one of these needs the process-level write opt-in, because on 2026-09-02
/// closing a stuck SOLIDWORKS modal through the window manager took the whole
/// application down. The flag exists so a caller can see which dialogs on screen
/// are the bridge's own mess.
///
/// Deliberately ONE entry: `export_options` is the sheet a `SaveAs3` raises.
/// Everything else, `settings` and `message` included, can equally be a dialog
/// the USER opened and is part-way through answering.
pub const BRIDGE_CAUSED_DIALOGS: &[&str] = &["export_options"];
/// Ask ab for the desktop's windows. Returns `Err` with a human message when
/// ab could not answer, so a caller can decide whether that is fatal (it never
/// is, here).
pub fn list_windows(timeout: Duration) -> Result<Vec<Value>, String> {
let resp = crate::caller::ad_callback("desktop_list_windows", json!({}), timeout);
windows_from(&resp).ok_or_else(|| {
resp.get("error")
.and_then(|e| e.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "ab returned no window list".to_string())
})
}
/// Dig the window array out of whichever envelope ab used. ab wraps some verb
/// payloads as a JSON STRING in `output` (`{"data":{"windows":[...]}}`) and
/// returns others inline, so both shapes are accepted rather than guessed at.
pub fn windows_from(resp: &Value) -> Option<Vec<Value>> {
fn dig(v: &Value) -> Option<Vec<Value>> {
v.get("windows")
.or_else(|| v.get("data").and_then(|d| d.get("windows")))
.and_then(|w| w.as_array())
.cloned()
}
if let Some(w) = dig(resp) {
return Some(w);
}
let inner: Value = serde_json::from_str(resp.get("output")?.as_str()?).ok()?;
dig(&inner)
}
/// How a SOLIDWORKS dialog title is classified. The point of the classification
/// is the `blocking` / `neverAutoAnswer` split: a bridge may report anything,
/// but it must not answer a question about the USER'S unsaved work.
pub fn classify(title: &str) -> (&'static str, bool, bool) {
let t = title.to_lowercase();
// (kind, blocking, neverAutoAnswer)
// The per-document prompt ("Save changes to Part1?") AND the multi-document
// one, which on a Connected edition is titled "Save Modified Documents" and
// offers Save all to This PC / Save all to 3DEXPERIENCE / Don't Save /
// Cancel. That second title contains neither "change" nor "?", so it used to
// classify as `unknown` — still never auto-answered, but the quit ladder
// would have reported it as a generic dialog instead of the one finding that
// matters: a save prompt at a VERIFIED zero open documents means the
// document count was wrong (docs/QUIT-INVESTIGATION.md §5.5).
if t.contains("save")
&& (t.contains("change") || t.contains("?") || t.contains("modified") || t.contains("document"))
{
return ("save_changes", true, true);
}
// IMPORT DIAGNOSTICS. Measured 2026-09-17: `swImportAutoRunImportDiagnostics`
// (291) and `swImportNeutralRunDiagnostics` (690) are both TRUE on this
// install, so a STEP with faulty faces raises this MODAL dialog during the
// import. It classified as `unknown` before — never auto-answered, which was
// right, but reported as a generic dialog when it is the single most likely
// thing to stop an import on this machine. It is still NEVER answered: the
// choices it offers (attempt to heal, or not) change the geometry the user
// gets, and that is theirs to make.
if t.contains("import diagnostic") || t.contains("import diagnos") {
return ("import_diagnostics", true, true);
}
if t.contains("recover") {
return ("document_recovery", true, true);
}
if t.contains("licen") || t.contains("activat") || t.contains("3dexperience") {
return ("license", true, true);
}
if t.contains("update") || t.contains("upgrade") {
return ("update", true, true);
}
if t == "export" || t.starts_with("export ") || t.contains("export options") {
return ("export_options", true, false);
}
if t.contains("units") || t.contains("template") || t.contains("options") {
return ("settings", true, false);
}
if t.contains("error") || t.contains("warning") || t.contains("rebuild") {
return ("message", true, false);
}
("unknown", true, true)
}
/// Is this window plausibly SOLIDWORKS'? `main_hwnd` (from COM's
/// `Frame().GetHWnd()`) identifies the application window exactly; a top-level
/// `#32770` cannot be attributed from the flat list ab returns, so it is
/// reported with `ownerConfirmed:false` rather than silently claimed.
pub fn is_solidworks_window(w: &Value, main_hwnd: Option<i64>, sw_pid: Option<i64>) -> Option<(bool, bool)> {
let hwnd = w.get("hwnd").and_then(|h| h.as_i64())?;
let title = w.get("title").and_then(|t| t.as_str()).unwrap_or("");
let class = w.get("className").and_then(|c| c.as_str()).unwrap_or("");
let pid = w.get("pid").and_then(|p| p.as_i64());
// PROOF, in the only two forms available:
// 1. the hwnd IS the application window COM told us about, or
// 2. the window's owner pid IS SOLIDWORKS' pid.
if Some(hwnd) == main_hwnd {
return Some((true, true));
}
if let (Some(p), Some(sw)) = (pid, sw_pid) {
if p == sw {
return Some((true, true));
}
// A pid we CAN see, that is not SOLIDWORKS', is not ours at all.
return None;
}
// Everything below is a CANDIDATE, never a confirmation.
//
// A title merely CONTAINING "SOLIDWORKS" used to count as proof, and it is
// not: the SOLIDWORKS Installation Manager, a 3DEXPERIENCE launcher dialog
// and an ordinary Explorer window showing a SOLIDWORKS folder all match,
// and none of them belong to the process this bridge is driving. With COM
// down there is no `main_hwnd` either, so the title would be the ONLY test —
// exactly when being wrong is most likely.
if title.to_uppercase().contains("SOLIDWORKS") {
return Some((true, false));
}
if class == DIALOG_CLASS {
return Some((true, false));
}
None
}
/// The dialog census: every window that could be a SOLIDWORKS modal, classified.
pub fn dialog_census(windows: &[Value], main_hwnd: Option<i64>, sw_pid: Option<i64>) -> Vec<Value> {
let mut out = Vec::new();
for w in windows {
let class = w.get("className").and_then(|c| c.as_str()).unwrap_or("");
let title = w.get("title").and_then(|t| t.as_str()).unwrap_or("");
if class != DIALOG_CLASS {
continue;
}
let Some((ours, confirmed)) = is_solidworks_window(w, main_hwnd, sw_pid) else { continue };
if !ours {
continue;
}
let (kind, blocking, never_auto) = classify(title);
out.push(json!({
"hwnd": w.get("hwnd").cloned().unwrap_or(Value::Null),
"title": title,
"className": class,
"kind": kind,
"bridgeCaused": BRIDGE_CAUSED_DIALOGS.contains(&kind),
"blocking": blocking,
"neverAutoAnswer": never_auto,
"ownerConfirmed": confirmed,
"rect": w.get("rect").cloned().unwrap_or(Value::Null),
}));
}
out
}
/// Every window we believe belongs to SOLIDWORKS, for `swx_window_info`.
pub fn solidworks_windows(windows: &[Value], main_hwnd: Option<i64>, sw_pid: Option<i64>) -> Vec<Value> {
windows
.iter()
.filter_map(|w| {
let (ours, confirmed) = is_solidworks_window(w, main_hwnd, sw_pid)?;
if !ours {
return None;
}
let class = w.get("className").and_then(|c| c.as_str()).unwrap_or("");
Some(json!({
"hwnd": w.get("hwnd").cloned().unwrap_or(Value::Null),
"title": w.get("title").cloned().unwrap_or(Value::Null),
"className": class,
"rect": w.get("rect").cloned().unwrap_or(Value::Null),
"isMain": w.get("hwnd").and_then(|h| h.as_i64()) == main_hwnd,
"isDialog": class == DIALOG_CLASS,
"isFrame": is_frame_window(w, main_hwnd),
// `closable` is what a caller ACTS on, so it has to fold in
// every refusal `swx_close_window` applies — not just
// attribution. Reporting a Save-changes dialog as closable was
// the same discoverability shape as handing out `mainHwnd`.
"closable": !is_frame_window(w, main_hwnd)
&& confirmed
&& !classify(w.get("title").and_then(|t| t.as_str()).unwrap_or("")).2,
"pid": w.get("pid").cloned().unwrap_or(Value::Null),
"ownerConfirmed": confirmed,
}))
})
.collect()
}
/// Close one window through ab's UIA WindowPattern.
pub fn close_window(hwnd: i64, reason: &str, timeout: Duration) -> Value {
crate::caller::ad_callback(
"desktop_ui_window",
json!({"action": "close", "hwnd": hwnd, "reason": reason}),
timeout,
)
}
/// Put (or clear) the "Adom is driving SOLIDWORKS" taskbar overlay badge.
pub fn set_badge(hwnd: i64, on: bool, tooltip: &str, timeout: Duration) -> Value {
crate::caller::ad_callback(
"desktop_taskbar",
json!({
"hwnd": hwnd,
"overlay": {"badge": if on { "adom" } else { "none" }, "tooltip": tooltip},
}),
timeout,
)
}
/// Background PrintWindow capture of one window — and, per ab's contract, of
/// every popup/dialog that window owns. That owned-popup list is the only
/// AUTHORITATIVE way to attribute a dialog to SOLIDWORKS, which is why
/// `swx_state`'s screenshot doubles as the trustworthy dialog census.
pub fn screenshot(hwnd: i64, timeout: Duration) -> Value {
crate::caller::ad_callback("desktop_screenshot_window", json!({"hwnd": hwnd}), timeout)
}
#[cfg(test)]
mod tests {
use super::*;
fn win(hwnd: i64, title: &str, class: &str) -> Value {
json!({"hwnd": hwnd, "title": title, "className": class, "rect": {"left": 0}})
}
#[test]
fn unwraps_both_envelopes_ab_actually_uses() {
// inline
let inline = json!({"windows": [win(1, "a", "X")]});
assert_eq!(windows_from(&inline).unwrap().len(), 1);
// the real one observed live: a JSON STRING under `output`
let nested = json!({"output": json!({"data": {"windows": [win(1, "a", "X"), win(2, "b", "Y")]}}).to_string()});
assert_eq!(windows_from(&nested).unwrap().len(), 2);
// and a shape with neither
assert!(windows_from(&json!({"error": "nope"})).is_none());
}
#[test]
fn the_dialogs_that_must_never_be_auto_answered_are_flagged() {
for (title, kind) in [
("SOLIDWORKS", "unknown"),
("Save changes to Part1?", "save_changes"),
("SOLIDWORKS Document Recovery", "document_recovery"),
("3DEXPERIENCE", "license"),
] {
let (k, blocking, never) = classify(title);
assert_eq!(k, kind, "{title}");
assert!(blocking, "{title}");
assert!(never, "{title} must never be auto-answered");
}
// An export option sheet is classified as answerable IN PRINCIPLE (it is
// the bridge's own dialog, not a question about the user's work) — but
// classification is only half the decision: closing also requires the
// window to be attributable to SOLIDWORKS. See
// `an_unattributed_dialog_is_never_closable`.
let (k, _, never) = classify("Export");
assert_eq!(k, "export_options");
assert!(!never);
}
#[test]
fn the_application_frame_is_never_closable() {
// THE round-7 hole. `ownerConfirmed` is true exactly when a window is
// most provably SOLIDWORKS' — and the main frame is the most provable
// window there is, so the "not a general window killer" guard passed on
// the one window that must never be closed this way. Closing a frame is
// quitting the application, which is swx_stop's job and is gated.
let main = Some(4242);
let frame = win(4242, "SOLIDWORKS Design Professional for Makers 2026", "Afx:0000");
assert!(is_frame_window(&frame, main), "the main window is a frame");
let ours = solidworks_windows(&[frame.clone()], main, None);
assert_eq!(ours[0]["isMain"], true);
assert_eq!(ours[0]["ownerConfirmed"], true, "it IS provably ours...");
assert_eq!(ours[0]["closable"], false, "...and it is still never closable");
// A SECOND SOLIDWORKS frame (a second document window) is a frame too,
// even without being the one COM told us about.
let other = win(5555, "SOLIDWORKS", "Afx:0001");
assert!(is_frame_window(&other, main));
// A dialog is not a frame — that is what this verb is for.
let mut dialog = win(858186, "Export", DIALOG_CLASS);
dialog["pid"] = json!(777);
assert!(!is_frame_window(&dialog, main));
assert_eq!(solidworks_windows(&[dialog], main, Some(777))[0]["closable"], true);
// ...but a dialog about the USER'S OWN WORK is never closable, however
// provably ours it is.
let mut theirs = win(9001, "Save changes to gearbox?", DIALOG_CLASS);
theirs["pid"] = json!(777);
let row = solidworks_windows(&[theirs], main, Some(777));
assert_eq!(row[0]["ownerConfirmed"], true);
assert_eq!(row[0]["closable"], false, "never advertise the user's own question as closable");
}
#[test]
fn only_dialogs_the_bridge_itself_raised_are_closable_without_the_opt_in() {
// `export_options` is the sheet our own SaveAs3 raises. `settings` and
// `message` can just as easily be something the USER opened and is
// part-way through answering.
assert!(BRIDGE_CAUSED_DIALOGS.contains(&"export_options"));
for user_owned in ["settings", "message", "unknown", "save_changes"] {
assert!(
!BRIDGE_CAUSED_DIALOGS.contains(&user_owned),
"{user_owned} may belong to the user"
);
}
}
#[test]
fn an_unattributed_dialog_is_never_closable() {
// THE regression test for the review finding. ab's flat window list
// carries no owner pid, so a bare `#32770` could belong to Word's Save
// dialog just as easily as to SOLIDWORKS. The census must mark it
// ownerConfirmed:false, and the close path must refuse it on that basis
// — the earlier version of this test asserted the opposite ("ours to
// close"), encoding the hazard as intended behaviour.
let windows = vec![win(858186, "Export", DIALOG_CLASS)];
let census = dialog_census(&windows, Some(1836650), None);
assert_eq!(census.len(), 1, "it IS reported");
assert_eq!(census[0]["ownerConfirmed"], false, "...but never as proven SOLIDWORKS'");
// A dialog whose title names SOLIDWORKS is attributable, and a #32770 is
// exactly how SOLIDWORKS' own dialogs present.
// A title containing "SOLIDWORKS" is NOT proof: the Installation
// Manager and a 3DEXPERIENCE launcher dialog both match it and neither
// is the process we are driving.
let named = vec![win(9, "SOLIDWORKS Installation Manager", DIALOG_CLASS)];
assert_eq!(dialog_census(&named, None, None)[0]["ownerConfirmed"], false);
// An owner pid IS proof, in both directions.
let mut owned = win(11, "Export", DIALOG_CLASS);
owned["pid"] = json!(4242);
assert_eq!(dialog_census(&[owned.clone()], None, Some(4242))[0]["ownerConfirmed"], true);
assert!(dialog_census(&[owned], None, Some(777)).is_empty(), "another app's dialog is not ours at all");
}
#[test]
fn attribution_is_reported_not_assumed() {
let main = Some(1836650);
let windows = vec![
win(1836650, "SOLIDWORKS Design Professional for Makers 2026", "Afx:0000"),
win(858186, "Export", DIALOG_CLASS),
win(266266, "gallia - Google Chrome", "Chrome_WidgetWin_1"),
];
let ours = solidworks_windows(&windows, main, None);
assert_eq!(ours.len(), 2, "Chrome is not ours");
assert_eq!(ours[0]["isMain"], true);
assert_eq!(ours[0]["ownerConfirmed"], true);
// a bare #32770 is a CANDIDATE, and says so
assert_eq!(ours[1]["isDialog"], true);
assert_eq!(ours[1]["ownerConfirmed"], false);
let dialogs = dialog_census(&windows, main, None);
assert_eq!(dialogs.len(), 1);
assert_eq!(dialogs[0]["kind"], "export_options");
assert_eq!(dialogs[0]["hwnd"], 858186);
}
#[test]
fn without_a_com_hwnd_the_main_window_is_still_found_by_title() {
let windows = vec![win(9, "SOLIDWORKS Design Professional", "Afx:1")];
let ours = solidworks_windows(&windows, None, None);
assert_eq!(ours.len(), 1, "it is still REPORTED as a candidate");
assert_eq!(ours[0]["isMain"], false, "unproven without the COM hwnd");
assert_eq!(
ours[0]["ownerConfirmed"], false,
"a title is not proof of ownership — and with COM down it would be the ONLY test"
);
}
}