main
Drew Owens 0.3.3 source + crate build: BOM exported from the SOLIDWORKS assembly, build log §1–11, release script 477e64f 1mo ago
#!/usr/bin/env python3
"""swx-batch.py <calls.json> [totalTimeout] [perCallTimeout] - run MANY bridge verbs in ONE round trip.

calls.json is a JSON array of {"verb":..., "args": {...}, "label": ...}. The array is pushed to
the desktop with ab's write_file (an EncodedCommand big enough to inline it blows the Windows
command-line length limit), then one small PowerShell loop POSTs each call to the dev bridge on
127.0.0.1:24695 and returns one JSON array of results.

Why this exists: every adom-bridge shell_execute round trip costs ~10-20s, and discovering a
late-bound COM signature takes dozens of probes.
"""
import base64, json, os, subprocess, sys

TARGET = ["--target", "adom_drew2", "--ai-thread", "SolidWorks Bridge"]
REQ = r"C:\Users\Public\swx_calls.json"


def ab(verb, args, timeout=300):
    """One ab call. NEVER raises.

    The `try` used to wrap only `json.loads`, so a `subprocess.TimeoutExpired`
    propagated out of here, through `cleanup()`, and past the invariant gate and
    every RESTORE — on exactly the runs (a wedged SOLIDWORKS, a dropped relay)
    where putting the machine back matters most.
    """
    try:
        r = subprocess.run(["adom-bridge"] + TARGET + [verb, json.dumps(args)],
                           capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        return {"success": False, "error": "adom-bridge timed out after %ss" % timeout}
    except BaseException as e:  # a missing CLI, a killed process, anything
        return {"success": False, "error": "%s: %s" % (type(e).__name__, e)}
    try:
        return json.loads(r.stdout)
    except Exception:
        return {"success": False, "raw": r.stdout[-2000:], "stderr": r.stderr[-500:]}


def run(calls, total=300, per=90, quiet=False, stop_on_failure=False):
    """Run `calls` in ONE round trip.

    `stop_on_failure` makes the desktop-side loop ABORT at the first call whose
    response is not `success:true`. That is not a convenience: a batch is a
    single trip, so without it a failed setup step (say, `swx_new_document`
    refusing) is followed by every later step in the list landing on whatever
    document happens to be active — which, in a batch aimed at a scratch part,
    is the user's. The remaining calls come back labelled `skipped`."""
    w = ab("write_file", {"path": REQ, "content": json.dumps(calls),
                          "reason": "Staging a batch of SOLIDWORKS bridge probe calls so they can be run in one round trip"})
    if not w.get("success", w.get("ok")):
        # NEVER exit from in here. live-smoke.py's cleanup and its
        # "documents unchanged" invariant run in a `finally`, and a SystemExit
        # raised from the batch runner would sail straight through them — the
        # invariant would never execute, on exactly the runs where it matters
        # most. Return None; the caller decides.
        print("write_file FAILED:", json.dumps(w)[:800])
        return None
    ps = (
        "$calls = Get-Content -Raw '" + REQ + "' | ConvertFrom-Json\n"
        "$out = @()\n"
        "$stop = $" + ("true" if stop_on_failure else "false") + "\n"
        "$halted = $false\n"
        "foreach ($c in $calls) {\n"
        "  if ($halted) { $out += @{ label = $c.label; verb = $c.verb; raw = '{\"success\":false,\"skipped\":true,\"error\":\"an earlier call in this batch failed\"}' }; continue }\n"
        "  $body = @{ command = $c.verb; args = $c.args } | ConvertTo-Json -Depth 12 -Compress\n"
        "  $p = \"$env:TEMP\\swx_req.json\"\n"
        "  [IO.File]::WriteAllText($p, $body)\n"
        "  $r = & curl.exe -s --max-time " + str(per) + " -H 'Content-Type: application/json' --data-binary \"@$p\" http://127.0.0.1:24695/command 2>&1 | Out-String\n"
        "  $r = $r.Trim()\n"
        "  $out += @{ label = $c.label; verb = $c.verb; raw = $r }\n"
        "  if ($stop) {\n"
        "    $okFlag = $false\n"
        "    try { $okFlag = (ConvertFrom-Json $r).success -eq $true } catch { $okFlag = $false }\n"
        "    if (-not $okFlag) { $halted = $true }\n"
        "  }\n"
        "}\n"
        "$out | ConvertTo-Json -Depth 6 -Compress\n"
    )
    enc = base64.b64encode(ps.encode("utf-16le")).decode()
    d = ab("shell_execute", {"command": "powershell -NoProfile -EncodedCommand " + enc,
                             "timeoutSeconds": total,
                             "reason": "Running a batch of SOLIDWORKS bridge verbs on your PC to validate them against the running SOLIDWORKS"},
           timeout=total + 60)
    out = (d.get("stdout") or "").strip()
    try:
        rows = json.loads(out)
    except Exception:
        print("RAW:", out[:3000], "| stderr:", (d.get("stderr") or "")[:600], "| status:", d.get("status"))
        return None
    if isinstance(rows, dict):
        rows = [rows]
    parsed = []
    for row in rows:
        raw = row.get("raw") or ""
        try:
            v = json.loads(raw)
        except Exception:
            v = {"success": False, "_raw": raw[:1200]}
        parsed.append({"label": row.get("label") or row.get("verb"), "verb": row.get("verb"), "result": v})
        if not quiet:
            print("=" * 8, parsed[-1]["label"])
            print(json.dumps(v, indent=1)[:2600])
    return parsed


if __name__ == "__main__":
    calls = json.load(open(sys.argv[1]))
    total = int(sys.argv[2]) if len(sys.argv) > 2 else 300
    per = int(sys.argv[3]) if len(sys.argv) > 3 else 90
    run(calls, total, per)