Skip to content

Releases: iagodpassos/autoitx

v0.1.2 — wait_for_any

Choose a tag to compare

@iagodpassos iagodpassos released this 01 Aug 18:36

wait_for_any — for actions with more than one outcome

An action in a legacy application rarely has one outcome. Committing a form
either closes it, raises an error dialog, or raises a different dialog meaning
something else entirely — and which one happened is the only way to know what
the application actually did.

AutoIt waits on one window at a time, so that race gets written by hand:

while (WinExists("Order Selection")
    && !WinExists("[CLASS:ui60Modal_W32]")
    && !WinExists("Blocked")) { Thread.Sleep(300); }

That loop is in production twice, in two separate robots, byte for byte the
same. Neither copy has a timeout: if none of the three ever happens, the
robot hangs until someone notices.

match ai.wait_for_any(&[
    (&orders,  WinCondition::Gone),   // the form closed: it saved
    (&modal,   WinCondition::Exists), // an error came up instead
    (&blocked, WinCondition::Exists), // ... or a block notice did
], Some(Duration::from_secs(60)))? {
    Some(0) => saved(),
    Some(1) => report_error()?,
    Some(2) => report_blocked()?,
    _ => return Err(wedged()),
}

Taking the timeout as a parameter is what removes the failure mode — there is
no version of this call that can hang. Returning the index rather than a bool is
the point: the caller has to branch on which outcome occurred.

WinCondition has four variants — Exists, Gone, Active, NotActive
mirroring AutoIt's four single-window waits, so racing them against each other
needs no vocabulary you do not already have.

Design notes

  • It is in the core, not recipes. That module is for portable intent
    whose mechanism differs per platform. This polls win_exists/win_active
    identically on both, and filing it there would have meant weakening the
    module's own charter. autoitx-sys and its 1:1 AU3_* surface are untouched.
  • Order is significant, and documented. Watches are evaluated in slice order
    and the lower index wins a tie. Without a stated rule the return value would
    depend on polling luck.
  • Polling runs at WinWaitDelay (250 ms by default) — the option that
    already paces the other waits, not a fresh literal.
  • An empty slice returns Ok(None) at once. Blocking would hang forever
    when the timeout is None, so the degenerate case is the one worth
    special-casing — and the one with a test, because getting it wrong hangs CI
    rather than failing it.
  • Ok(None) on timeout mirrors win_wait returning Ok(false), rather
    than an Err(Timeout) that reads as more expressive but less consistent.

Compatibility

Purely additive. Anyone on autoitx = "0.1" picks this up without editing a
manifest — hence 0.1.2 rather than 0.2.0.

autoitx 0.1.1

Choose a tag to compare

@iagodpassos iagodpassos released this 28 Jul 12:07

No library changes. Documentation and crates.io metadata only — upgrading from 0.1.0 requires nothing.

[dependencies]
autoitx = "0.1"

Ten dead links on crates.io

crates.io does not serve the README from this repository. It renders the markdown itself and rewrites relative links against a base it derives on its own — and for this workspace that base was wrong twice over.

autoitx/Cargo.toml points at ../README.md, and the rewrite prepends the package directory regardless, so a link to the examples folder shipped as:

https://github.com/iagodpassos/autoitx/blob/HEAD/autoitx/autoitx/examples
                                                 ^^^^^^^ ^^^^^^^

Relative links are also rewritten to /blob/ unconditionally, which cannot address a directory even with the correct base.

Every one of those links resolved correctly on GitHub, which is why 0.1.0 shipped with them. Heading anchors had the mirror-image problem: comrak and GitHub both id headings user-content-<slug>, and only GitHub ships the JavaScript that additionally resolves the bare #<slug>, so #platform-support worked in one place and not the other.

Every link is now absolute and every anchor prefixed. The MSRV badge also pointed at the Rust blog index rather than the 1.85 release post — the URL it used answered 200, but only after redirecting to a stub, the blog having changed its URL scheme.

A check, since none of this was visible before publishing

scripts/check-readme-links.py now runs in the lint job. It rejects relative links, verifies that every link into this repository names a path that actually exists with the right /blob/ or /tree/, requires the portable anchor form, and catches reference-style links that were defined but never used — those render as literal bracketed text instead of failing.

Pointed at the 0.1.0 README, it reports all fourteen.

Its --network mode fetches every outbound URL and is deliberately kept out of CI, where someone else's downtime would fail the build. Two things it needs that are worth knowing if you write something similar: crates.io answers 404 for a crate that plainly exists unless you send Accept: text/html, and 403 if you send no User-Agent at all.

macOS in the metadata

The crate supported macOS natively from 0.1.0, but said so nowhere crates.io indexes. Added macos to the keywords, which cost gui — five is the maximum, and gui is browsed by people building interfaces rather than driving someone else's, while macos next to windows is the one thing separating this from every other AutoIt binding.

Categories gained os::windows-apis and os::macos-apis beside the generic os.

Binaries

diagnose for Windows and both Mac architectures is attached below, unchanged in behaviour from 0.1.0. Run it first when something is wrong: it prints the DLL search order with a mark against each candidate on Windows, and which privacy grants the binary holds on macOS. The macOS builds are ad-hoc signed; verify with SHA256SUMS.txt.


📦 crates.io · 📖 docs.rs · ☕ Buy me a coffee

v0.1.0 — AutoItX's API in Rust, on Windows and macOS

Choose a tag to compare

@iagodpassos iagodpassos released this 28 Jul 00:07

AutoItX's API, in Rust, on Windows and macOS.

autoitx drives other applications' user interfaces — keystrokes, mouse, clipboard, windows, processes. The API is modeled on AutoItX, so existing AutoIt automation ports over almost mechanically. Unlike AutoItX, it also runs natively on macOS.

[dependencies]
autoitx = "0.1"

📦 crates.io · 📖 docs.rs

Two backends, one API

Windows macOS
Mechanism AutoItX3_x64.dll, loaded at runtime Native — Accessibility, CGEvent, NSPasteboard
Extra install The DLL (ships with AutoIt) Nothing

All 117 AU3_* entry points are reachable, and AutoIt::raw() gets at the ones without a safe wrapper.

Two bugs it makes impossible

Keystroke injection. Send interprets {}!+^#, so interpolating a name, a price, or a password into a send string lets that data execute as key commands. A password containing { is a live bug, not a theoretical one.

ai.send(Keys::text(&password))?;              // escaped, always
ai.send(keys!("{CTRLDOWN}c{CTRLUP}"))?;       // validated at compile time

keys! runs a const fn validator, so a typo like {CTRLDWN} fails the build — no proc macro, no build-time cost.

The clipboard race. Reading a screen you cannot query means select, copy, read — and the usual way to know when the copy landed is a sentinel:

AutoItX.ClipPut("NO-VALUE");
AutoItX.Send("^c");
if (AutoItX.ClipGet() == "NO-VALUE") { /* assume nothing copied */ }

Three failure modes, all of which happen: the cell genuinely contains the sentinel, the copy rewrites the value that was already there so nothing appears to change, or the copy never happened and the stale clipboard is returned as this field's value.

recipes::read_screen_text waits on the OS clipboard sequence number instead — GetClipboardSequenceNumber on Windows, NSPasteboard.changeCount on macOS. It cannot collide with a real value, it notices identical rewrites, and a copy that never happened is reported rather than papered over.

Platform gaps are compile errors

What exists on only one platform lives in ext::windows / ext::macos. Using one from the wrong platform fails to build — it is not an Err(Unsupported) discovered at runtime, by which point a robot has half-completed a transaction in someone's ERP.

Where both platforms can do the same thing by different means, recipes gives one call. wait_until_idle polls the cursor shape on Windows (the idiom hand-written automation spells cursor == 2 || cursor == 5, here with the timeout that version invariably lacks) and probes the Accessibility messaging timeout on macOS. Your code says wait_until_idle.

Develop on a Mac, ship to Windows

The DLL is loaded at runtime, so there is no link-time Windows dependency and cargo check/clippy never invoke a linker. The entire Windows backend is type-checked, linted, and unit-tested from macOS — against a mock DLL that exercises all 117 signatures, UTF-16 marshalling, output-buffer growth, and the AU3_error protocol. A Windows machine is needed only to observe real behaviour, never to compile.

Verified against reality

The Windows backend's failure semantics were measured, not assumed. Calling each function against a window that exists and one that does not revealed that AutoItX has no single convention: some report failure in the return value, some in the error flag, some not at all, and WinGetProcess returns 0xFFFFFFFF rather than 0. WinGetPos's integer return is inverted. That table now sits at the top of backend/dll.rs, because the information exists nowhere else — the published documentation does not describe it.

The macOS backend has a live suite driving real applications, which caught four bugs a mock could never have found:

  • NSWorkspace::runningApplications returns a list kept current by run-loop notifications, and an automation binary never runs a run loop. Applications launched after startup were invisible for the life of the process, so "launch the app, wait for its window" waited forever.
  • {CTRLDOWN}c{CTRLUP} posted a correctly flagged event that applications ignored: macOS resolves a key equivalent from the virtual key code, and the Unicode-string path used for literal text carries key code 0.
  • is_active read AXMain, which the focused window reports as false while AXFocused is true — Electron apps and VM bridges never set it.
  • visible_frame returned None off the main thread, which is where automation runs, so win_set_state(Maximize) silently did nothing.

Getting started

cargo run --example diagnose is the first thing to run when something is wrong. It prints the DLL search order with a mark against each candidate on Windows, and which privacy grants the binary holds on macOS — the two failures that cost the most time, and both of which currently surface as something else entirely.

It is attached below as a prebuilt binary for Windows and both Mac architectures, so you can get an answer without installing a Rust toolchain. The macOS builds are ad-hoc signed; verify with SHA256SUMS.txt.

Eight examples ship in the repo, including a side-by-side porting guide from AutoItX.Dotnet.

Notes

MSRV 1.85. Licensed MIT OR Apache-2.0. 64-bit only — the DLL is x64, though Windows on ARM works fine under emulation.

The AutoItX3 DLL is not redistributed with this crate: AutoIt is freeware under a EULA, not an open-source licence. This project is not affiliated with, endorsed by, or sponsored by AutoIt Consulting Ltd. See NOTICE.

If it saves you time, you can buy me a coffee. ☕