← Commit history

0.2.41: licence_expired catalog entry + native WM_CLOSE fallback for UIA-refusing OK boxes

Colby Knox ·5230f683e8 ·1mo ago ·parent cc561f3
Co-Authored-By: Claude Fable 5 <[email protected]>
3 files changed +28−3
bridge-rs/src/dialogs.rs+11−1
@@ -54,6 +54,8 @@ const CATALOG: &[(&str, &str, &str, &str)] = &[     ("file type not recognized", "unsupported_document", "OK", "Same as 'not recognised' (US spelling)."),     ("license maximum usage reached", "licence_seat_stranded", "OK",      "The seat is held by an instance that never released it — almost always an Altium that was KILLED rather than closed. The licence page will still show Seats 0/1, which looks free and is not. FIX: close every other Altium POLITELY (altium_close, never process_kill), then altium_license {action:'use_license'}. Waiting does not clear it."),+    ("has expired or been deactivated", "licence_expired", "OK",+     "The licence (Develop entitlement or a lease) is dead server-side: Altium runs but refuses scripting and can wedge in a modal loop on close. The bridge cannot renew an entitlement - the human renews or re-activates it in the Altium workspace. NOTE: Develop shows NO title licence tail in this state either, so the absent-tail 'Licensed' inference is WRONG while this box is up (measured 2026-09-21)."),     ("license renewal failed", "licence_seat_stranded", "OK",      "Altium could not renew the On-Demand lease. Same cause and fix as 'maximum usage reached': a killed instance is still holding the seat. This dialog RE-FIRES on a timer until the seat is reclaimed, so clearing it is not the fix."),     ("cannot focus a disabled or invisible window", "altium_internal", "Send and Close",@@ -224,10 +226,18 @@ fn is_licence_chooser(btns: &[String]) -> bool {     l.iter().any(|b| b == "choose a license") && l.iter().any(|b| b == "continue") } -/// Press a named button on a dialog through UIA (no focus steal, no cursor).+/// Press a named button on a dialog through UIA (no focus steal, no cursor). When the button is+/// an acknowledgement (OK) and its UIA Invoke refuses - some Information boxes expose no pattern+/// (measured 2026-09-21 on "licence has expired or been deactivated") - fall back to a native+/// WM_CLOSE on the dialog: the same dismissal its titlebar X performs. Never for any other+/// button: closing a question box would silently answer it with its cancel path. pub fn press(hwnd: i64, button: &str) -> Value {     let r = ab::call("desktop_ui_click", json!({"hwnd": hwnd, "name": button, "role": "button"}), &[], 15);     let ok = r.get("success").and_then(|b| b.as_bool()).unwrap_or(false);+    if !ok && button.eq_ignore_ascii_case("ok") && crate::win32::post_message_close(hwnd) {+        return json!({"success": true, "clicked": button, "hwnd": hwnd, "via": "wm_close_fallback",+            "note": "the OK exposed no UIA Invoke; dismissed with a native WM_CLOSE (same as its titlebar X)", "ab": r});+    }     json!({"success": ok, "clicked": button, "hwnd": hwnd, "via": "uia", "ab": r}) } 
bridge-rs/src/win32.rs+16−1
@@ -28,9 +28,19 @@ mod imp {         fn IsHungAppWindow(hwnd: Hwnd) -> i32;         fn IsWindowEnabled(hwnd: Hwnd) -> i32;         fn PostThreadMessageW(tid: u32, msg: u32, wparam: usize, lparam: isize) -> i32;+        fn PostMessageW(hwnd: Hwnd, msg: u32, wparam: usize, lparam: isize) -> i32;     }      const WM_QUIT: u32 = 0x0012;+    const WM_CLOSE: u32 = 0x0010;++    /// Post WM_CLOSE to one window — the dismissal its titlebar X performs, POSTED so it cannot+    /// inherit a hang. The dialog fallback lane: some Information boxes carry an OK that exposes+    /// no UIA Invoke (measured 2026-09-21 on the expired-licence box), and closing the box IS the+    /// acknowledgement.+    pub fn post_message_close(hwnd: i64) -> bool {+        unsafe { PostMessageW(hwnd as Hwnd, WM_CLOSE, 0, 0) != 0 }+    }      /// Ask a UI thread to leave its message loop — Delphi's `Application.Terminate`, reached from     /// outside. POSTED (never sent), so it cannot inherit a hang, and unlike TerminateProcess the@@ -174,7 +184,7 @@ mod proc_imp { }  #[cfg(windows)]-pub use imp::{post_quit_to_thread, visible_windows, WinRow};+pub use imp::{post_message_close, post_quit_to_thread, visible_windows, WinRow}; #[cfg(windows)] pub use proc_imp::{kill, pids_of}; @@ -188,6 +198,11 @@ pub fn kill(_pid: u32) -> bool {     false } +#[cfg(not(windows))]+pub fn post_message_close(_hwnd: i64) -> bool {+    false+}+ #[cfg(not(windows))] pub fn post_quit_to_thread(_tid: u32) -> bool {     false
page.json+1−1
@@ -140,4 +140,4 @@     "skills/altium-wiki-import/SKILL.md",     "uninstall.sh"   ]-}\ No newline at end of file+}