app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
#!/usr/bin/env python3
"""launch-gate.py — prove the launch state machine on the REAL machine.
RUN THIS ONLY WHEN DREW SAYS SOLIDWORKS IS DOWN. It starts SOLIDWORKS on his
desktop. It never stops, kills or foregrounds anything.
WHAT IT MEASURES, and why it measures rather than reads
───────────────────────────────────────────────────────
Every claim here is checked against the MACHINE — the process list and the
launcher's own log files — not against what the bridge returned. The bug this
whole release exists for returned `stillLaunching:true` with a hint promising
"call swx_start again, which ATTACHES rather than launching a second copy", and
every one of those calls started another launcher chain. The return value was
right and the effect was wrong, four times over.
1. BASELINE the CATSTART / SWXDesktopLauncher / 3DEXPERIENCELauncher*
processes and the launcher logs that exist BEFORE we touch
anything. Everything after is measured as a delta from this.
2. LAUNCH swx_start with a reason and waitSeconds:0.
3. ONE CHAIN exactly ONE new CATSTART+SWXDesktopLauncher pair appears.
4. RUN BEGIN the NEW launcher log logs `Run Begin` within 20s.
5. IDEMPOTENCE swx_start x3 during flight -> ZERO new chains, zero new logs.
6. ATTACH COM attaches within 240s.
7. UNTOUCHED every pre-existing process from the baseline is still alive,
every 3DEXPERIENCELauncher* daemon included.
8. TIMINGS per-stage seconds, and which route won.
9. CAPTURE the REAL reply shape of ab's `desktop_find_control`, which
crash-reporter recovery reads to decide whether to click.
That shape has never been observed, which is why the feature
ships DISABLED (`launch.crashRecovery == "off"`). The probe
is read-only and clicks nothing.
10. WHICH WORLD before anything else, the gate asks whether route
`shellExecute` comes back with a `rootPid`. That single
answer decides whether this bridge can EVER clean up
after itself on this machine (see "no handle, no kill"),
and it is the one fact the whole design turns on.
tools/launch-gate.py # the full gate
tools/launch-gate.py --baseline # print the baseline and exit (safe)
tools/launch-gate.py --probe-uia # ONLY the read-only UIA capture (safe)
tools/launch-gate.py --probe-handle-launches # ONLY the attribution pre-check
# (the name is a warning: it LAUNCHES SOLIDWORKS)
Talks to the bridge exactly as tools/swx-live.py does: PowerShell on the
desktop, over adom-bridge shell_execute. Read-only desktop probes need no
special permission; the one action is swx_start itself.
"""
import base64
import json
import subprocess
import sys
import time
# THE ROUTE: verbs go through adom-bridge's verb routing to the bridge it hosts
# (see `verb`); SWX_BRIDGE_URL forces a loopback /command URL instead. The old
# default, http://127.0.0.1:24695/command, is dead since adom-bridge 2.1.163.
THREAD = "SolidWorks Bridge"
PS_FAST_EMPTY_RETRIES = 3
# How long after the start SOLIDWORKS may take to report READY (attach + add-ins +
# the 3DEXPERIENCE platform load; the control launch of 2026-10-01 logged
# <3DXLOAD_TIME>24000).
READY_BUDGET = 420
PS_RETRY_BACKOFF_SECS = 2
CHAIN = ("CATSTART", "SWXDesktopLauncher")
NEVER_TOUCH = ("3DEXPERIENCELauncher", "3DEXPERIENCELauncherSysTray", "3DEXPERIENCELauncherService")
RUN_BEGIN_BUDGET = 25 # the bridge's own budget is 20s per route
ATTACH_BUDGET = 260 # the bridge's own budget is 240s
POLL = 3
class BlockedOnSignIn(Exception):
"""The launch is waiting for the USER to sign in to 3DEXPERIENCE. Not a
failure of any claim — nothing after attach can be measured until a person
answers the window. Exit code 4, naming the window (0.9.12; measured
2026-10-02: reported as five FAILs, "COM attached never")."""
def attach_verdict(attached_at, last_status):
"""("attached" | "signIn" | "failed", detail) from the attach loop's outcome."""
if attached_at is not None:
return ("attached", "%.1fs" % attached_at)
st = last_status if isinstance(last_status, dict) else {}
if st.get("waitingOn") == "userSignIn":
return ("signIn", st.get("signInWindow") or "(untitled sign-in window)")
return ("failed", "never")
class Unreadable(Exception):
"""A probe could not be ANSWERED. Not an empty answer — no answer.
Every one of this gate's claims is about the machine, so a probe that
cannot be read makes the claim it would have supported UNPROVEN. The first
version returned `{}` and `""` on failure, which is the same
empty-to-confident collapse the bridge itself was rewritten four times to
stop making: "could not ask" was read downstream as "nothing found", and a
gate that cannot see the machine would have reported PASS.
"""
# EVERY SCRIPT SAYS WHY IT FAILED (2026-10-01). A shell_execute that exits 1 with
# empty stdout AND stderr was read for two days as a "transient" and retried; it
# was never transient — on 09-30 it was curl.exe against the dead loopback port
# (curl exit 7 surfacing as an empty exit 1), and on 10-01 it was the launch
# gate's UIA probe, every single time. So every script now runs inside a wrapper
# that PRINTS its own failure to stdout: a trap for terminating errors, and on a
# non-zero native exit the exit code and every error PowerShell recorded. Empty
# stderr cannot hide a failure again. ($ErrorActionPreference is deliberately left
# at its default: making it Stop would turn the non-terminating errors the
# measured scripts already tolerate into new failures.)
def _wrap(script):
return (
"$Error.Clear(); "
"trap { [Console]::Out.WriteLine('PS-ERROR: ' + ($_ | Out-String).Trim()); exit 1 }; "
+ script +
"\n$__code = $LASTEXITCODE; "
"if ($__code) { [Console]::Out.WriteLine('PS-NATIVE-EXIT: ' + $__code); "
"foreach ($__e in $Error) { [Console]::Out.WriteLine('PS-ERROR: ' + ($__e | Out-String).Trim()) }; "
"exit $__code }"
)
def ps(script, reason, timeout=120, _attempt=0):
"""Run PowerShell on Drew's desktop and return its stdout.
NOTE ON `shell_execute`: this is ab's general escape hatch — it runs an
ARBITRARY command on the user's machine, and everything below rides on it.
Nothing in this file constrains what could be sent; the reader's assurance
is reading the script. The commands here are: `Get-CimInstance Win32_Process`
(a process census), `Get-ChildItem`/`Select-String` over the launcher's own
log directory, and `curl.exe` to the bridge's loopback port. There is no
kill, no write outside $env:TEMP, and no path outside %APPDATA%\\DassaultSystemes.
"""
enc = base64.b64encode(_wrap(script).encode("utf-16le")).decode()
r = subprocess.run(
[
"adom-bridge", "--target", "adom_drew2", "--ai-thread", THREAD, "shell_execute",
json.dumps({
"command": "powershell -NoProfile -EncodedCommand " + enc,
"timeoutSeconds": timeout + 30,
"reason": reason,
}),
],
capture_output=True, text=True,
)
try:
payload = json.loads(r.stdout)
except Exception:
raise Unreadable("adom-bridge did not return JSON: %s%s"
% (r.stdout[:200], (" / " + r.stderr[:200]) if r.stderr else ""))
if payload.get("success") is False:
# A FAST, EMPTY exit 1 (under 5 s, nothing on stdout or stderr). It was
# called "transient" and it was not: on 2026-09-30 it was curl.exe against
# the dead loopback port (exit 7, surfacing as an empty exit 1), and on
# 2026-10-01 it was the launch gate's UIA probe, deterministically — four
# attempts in a row. Since `_wrap`, a script that fails prints WHY, so a
# truly empty failure means the script never ran at all; the bounded
# retry (three times, 2 s apart, each printed) stays for that case only.
# Any other failure, or a fourth, is Unreadable — with the script's own
# error text.
fast_empty = (payload.get("exitCode") == 1 and not (payload.get("stdout") or "")
and not (payload.get("stderr") or "") and (payload.get("durationMs") or 99999) < 5000)
if fast_empty and _attempt < PS_FAST_EMPTY_RETRIES:
print(" (shell_execute failed fast with no output; retry %d of %d in %ds)"
% (_attempt + 1, PS_FAST_EMPTY_RETRIES, PS_RETRY_BACKOFF_SECS))
time.sleep(PS_RETRY_BACKOFF_SECS)
return ps(script, reason, timeout, _attempt=_attempt + 1)
said = "\n".join(l for l in (payload.get("stdout") or "").splitlines()
if l.startswith("PS-ERROR: ") or l.startswith("PS-NATIVE-EXIT: "))
raise Unreadable("shell_execute failed (exit %s): %s%s"
% (payload.get("exitCode"), said or "no error text at all — the script never ran",
"" if said else " / " + json.dumps(payload)[:200]))
out = payload.get("stdout")
if out is None:
raise Unreadable("shell_execute returned no stdout: %s" % json.dumps(payload)[:300])
return out.strip()
def verb(name, args=None, timeout=120):
"""Call a bridge verb THROUGH the desktop bridge's verb routing (adom-bridge
routes swx_* to the bridge instance it hosts). Raises `Unreadable` rather
than inventing an answer.
MEASURED 2026-09-30: adom-bridge 2.1.163 hosts the released bridge itself
(bridges-cache\\solidworks, its own port) and terminates a side-loaded copy
on 24695 within 30 s, so the loopback route this helper used until 0.8.9 is
dead. Set SWX_BRIDGE_URL to a loopback /command URL to force the old route.
"""
import os
routed = not os.environ.get("SWX_BRIDGE_URL")
if not routed:
body = json.dumps({"command": name, "args": args or {}})
script = (
"$b=@'\n" + body + "\n'@; $p=\"$env:TEMP\\swx_gate.json\"; "
"[IO.File]::WriteAllText($p,$b); "
f"curl.exe -s --max-time {timeout} -H \"Content-Type: application/json\" "
f"--data-binary \"@$p\" {os.environ['SWX_BRIDGE_URL']}"
)
out = ps(script, f"SOLIDWORKS gate: calling {name}", timeout)
else:
# A VERB THAT DOES NOT ANSWER IS 'could not ask', not a crash. Without
# this, subprocess.TimeoutExpired escaped as a traceback — a gate dying
# on a slow verb before it could print a verdict or clean up.
try:
r = subprocess.run(
["adom-bridge", "--target", "adom_drew2", "--ai-thread", THREAD, name,
json.dumps(args or {})],
capture_output=True, text=True, timeout=timeout + 60,
)
except subprocess.TimeoutExpired:
raise Unreadable("%s did not answer through adom-bridge within %ds" % (name, timeout + 60))
out = r.stdout
i = out.find("{")
if i < 0:
raise Unreadable("%s did not answer with JSON: %r%s" % (name, out[:200], (" / " + r.stderr[:200]) if r.stderr else ""))
out = out[i:]
try:
d = json.loads(out)
except Exception:
raise Unreadable("%s did not answer with JSON: %r" % (name, out[:300]))
# adom-bridge wraps a ROUTED reply as {"status":..,"result":{...}} (and merges
# top-level fields and adds an `output` string); the bridge's own reply is the
# `result`. Only the routed reply is unwrapped: on the loopback route the JSON
# IS the bridge's reply, and a verb field that happens to be called `result`
# is not a wrapper.
if routed and isinstance(d, dict) and isinstance(d.get("result"), dict):
return d["result"]
return d
# ── the machine, measured ───────────────────────────────────────────────────
PROBE = r"""
$rows = Get-CimInstance Win32_Process -Filter "Name='CATSTART.exe' or Name='SWXDesktopLauncher.exe' or Name='sldworks.exe' or Name like '3DEXPERIENCELauncher%'" |
ForEach-Object { [pscustomobject]@{ pid=$_.ProcessId; name=$_.Name; started=$_.CreationDate } }
$logdir = "$env:APPDATA\DassaultSystemes\SWXDesktopLauncher"
$logs = @()
if (Test-Path $logdir) {
$logs = Get-ChildItem $logdir -Filter *.log | Where-Object { $_.Name -notlike '*_dslcc*' } |
ForEach-Object {
[pscustomobject]@{ name=$_.Name; created=$_.CreationTime.ToString('o');
runBegin=[bool](Select-String -Path $_.FullName -Pattern 'Run Begin' -Quiet) }
}
}
$helperdir = "$env:APPDATA\DassaultSystemes\SWXDesktopHelper"
$helpers = @()
if (Test-Path $helperdir) {
$helpers = Get-ChildItem $helperdir -Filter *.log | Where-Object { $_.Name -notlike '*_dslcc*' } |
ForEach-Object { $_.Name }
}
@{ procs=@($rows); logs=@($logs); helpers=@($helpers) } | ConvertTo-Json -Depth 5 -Compress
"""
def probe(why="reading the process list and launcher logs (read-only)"):
out = ps(PROBE, "SOLIDWORKS launch gate: " + why, 90)
try:
d = json.loads(out)
except Exception:
raise Unreadable("the process/log probe did not answer with JSON: %r" % out[:300])
def listy(v):
return v if isinstance(v, list) else ([] if v is None else [v])
return {
"procs": listy(d.get("procs")),
"logs": listy(d.get("logs")),
"helpers": listy(d.get("helpers")),
}
def pids(snap, prefixes):
return {int(p["pid"]) for p in snap["procs"] if str(p.get("name", "")).startswith(prefixes)}
def chain_pids(snap):
return pids(snap, CHAIN)
def log_names(snap):
return {l["name"] for l in snap["logs"]}
def run_begin(snap, names):
return {l["name"] for l in snap["logs"] if l["name"] in names and l.get("runBegin")}
FAILURES = []
NOTES = []
def check(ok, claim, detail=""):
print((" PASS " if ok else " FAIL ") + claim + (f" [{detail}]" if detail else ""))
if not ok:
FAILURES.append(claim + (f" [{detail}]" if detail else ""))
return ok
def optional_step(name, fn):
"""AN OPTIONAL CAPTURE NEVER ABORTS A GATE whose claims passed (2026-10-01:
the UIA capture — the gate's last, read-only step — made every run exit 3).
A failure is printed with its reason and recorded as a NOTE, not a FAIL."""
try:
fn()
return True
except Unreadable as e:
print(" %s could not be captured: %s" % (name, e))
NOTES.append("%s (optional, read-only) could not be captured: %s" % (name, e))
return False
def self_test():
"""Check the gate's own plumbing against canned failures — no machine."""
def boom():
raise Unreadable("shell_execute failed (exit 1): PS-ERROR: simulated")
before = len(FAILURES)
ok = optional_step("UIA reply capture", boom) is False and len(FAILURES) == before \
and any("UIA reply capture" in n for n in NOTES)
# And ps() really sends the WRAPPED script: capture what it would run.
captured = {}
class _Reply:
stdout = json.dumps({"success": True, "exitCode": 0, "stdout": "ok"})
stderr = ""
returncode = 0
real_run = subprocess.run
def _fake(args, **_kw):
captured["args"] = args
return _Reply()
subprocess.run = _fake
try:
ps("Get-Date", "launch gate self-test")
finally:
subprocess.run = real_run
sent = base64.b64decode(json.loads(captured["args"][-1])["command"].split()[-1]).decode("utf-16le")
ok = ok and "trap" in sent and "PS-NATIVE-EXIT" in sent and "Get-Date" in sent
# The sign-in verdict (0.9.12): a launch waiting on the user's sign-in is its
# own outcome, never "COM attached never".
ok = ok and attach_verdict(None, {"waitingOn": "userSignIn",
"signInWindow": "Login | 3DEXPERIENCE ID | Dassault Systemes"}) \
== ("signIn", "Login | 3DEXPERIENCE ID | Dassault Systemes")
ok = ok and attach_verdict(None, {"launch": {"stage": "attaching"}})[0] == "failed"
ok = ok and attach_verdict(12.3, {"waitingOn": "userSignIn"})[0] == "attached"
print("\n" + "=" * 72)
print("SELF-TEST PASSED" if ok else "SELF-TEST FAILED")
return 0 if ok else 1
def probe_find_control():
"""CAPTURE the reply shape of desktop_find_control. Read-only: it looks for
a control and reports what came back. Nothing is clicked, ever."""
print("\nUIA REPLY CAPTURE (read-only)")
wins = verb("swx_window_info", {}, 60)
rows = wins.get("windows")
if not isinstance(rows, list):
print(" window list UNKNOWN — " + str(wins.get("abError") or wins.get("error")))
return
target = None
for w in rows:
if w.get("isMain"):
target = w
break
if target is None and rows:
target = rows[0]
if target is None:
print(" no SOLIDWORKS window to probe")
return
body = json.dumps({
"command": "desktop_find_control",
"args": {"hwnd": target.get("hwnd"), "contains": "File", "role": "menuitem"},
})
# SAYS WHY, ALWAYS (2026-10-01). This step failed deterministically — exit 1,
# empty output, ~300 ms, on every run — and the gate could not say why. The
# script now reports its own failure: no ab direct-API address in this
# shell, curl's exit code and output, or the exception. An empty answer is a
# valid answer and is said as one.
script = (
"try { "
"if (-not $env:ADOM_DIRECT_API_URL) { "
"'UIA-CAPTURE: ADOM_DIRECT_API_URL is not set in this shell - ab''s direct API is "
"not reachable from shell_execute, so desktop_find_control cannot be asked from here'; "
"exit 0 }; "
"$b=@'\n" + body + "\n'@; $p=\"$env:TEMP\\swx_uia.json\"; "
"[IO.File]::WriteAllText($p,$b); "
"$o = curl.exe -s --max-time 40 -H \"Content-Type: application/json\" "
"--data-binary \"@$p\" \"$env:ADOM_DIRECT_API_URL/command\" 2>&1 | Out-String; "
"if ($LASTEXITCODE -ne 0) { 'UIA-CAPTURE: curl exit ' + $LASTEXITCODE + ': ' + $o; exit 0 }; "
"if (-not $o.Trim()) { 'UIA-CAPTURE: an empty reply (no matching control)'; exit 0 }; "
"$o "
"} catch { 'UIA-CAPTURE: ' + $_.Exception.GetType().Name + ': ' + $_.Exception.Message; exit 0 }"
)
out = ps(script, "SOLIDWORKS launch gate: capturing ab's desktop_find_control reply shape "
"(read-only; nothing is clicked)", 60)
print(" hwnd " + str(target.get("hwnd")) + " -> " + (out[:1200] or "(no reply)"))
print(" ^ THIS is the shape `launch::mentions_button` must read. Until it is known,")
print(" crash-reporter recovery stays off (SWX_CRASH_RECOVERY is not set).")
# THE CRASH REPORTER IS A SEPARATE PROCESS. Windows Error Reporting hosts
# it (WerFault.exe / a Dassault reporter exe), so its owner pid is NOT
# sldworks.exe's — the attribution rule must accept a child or a related
# process, and to write that rule we need to see the real relationship.
print("\n every window whose title reads like a crash reporter, with its OWNER PID:")
seen = False
for w in rows:
t = str(w.get("title") or "").lower()
if "solidworks" in t and any(k in t for k in
("crash", "stopped", "not responding", "error report", "recovery")):
seen = True
print(" pid=%s hwnd=%s class=%s %r" %
(w.get("pid"), w.get("hwnd"), w.get("className"), w.get("title")))
sw_pid = verb("swx_status", {}, 40).get("solidworks", {}).get("processId")
print(" sldworks.exe pid = %s" % sw_pid)
if not seen:
print(" (none on screen right now — re-run this after a crash to capture the pair)")
def which_world():
"""DOES `shellExecute` HAND BACK A ROOT ON THIS MACHINE? **THIS LAUNCHES.**
`ShellExecuteExW` with SEE_MASK_NOCLOSEPROCESS returns a process handle for
what it started — unless the shell forwards the link to an already-running
process (DDE), in which case there is no handle and no attribution, and this
bridge will never terminate anything on this machine. Everything about the
cleanup half of the design depends on which of those two worlds we are in,
and nobody has ever looked. This STARTS A LAUNCH, so it needs SOLIDWORKS
down, same as the full gate."""
print("\nWHICH WORLD: can route 'shellExecute' say what it started?")
r = verb("swx_start", {
"reason": "launch-gate pre-check: does the shellExecute route report a root pid",
"waitSeconds": 0,
}, 90)
launch = verb("swx_status", {}, 40).get("launch", {})
attr = launch.get("attribution")
print(" route = %s" % launch.get("route"))
print(" attributed = %s" % launch.get("attributed"))
print(" attribution = %s" % json.dumps(attr))
print(" swx_start said = stage %s / %s" % (r.get("stage"), r.get("_hint", "")[:60]))
if attr and attr.get("pid"):
print("\n ATTRIBUTABLE WORLD: the invoke handed back pid %s (created %s, via %s)."
% (attr.get("pid"), attr.get("createdAt"), attr.get("source")))
print(" The bridge can clean up after a stall on this route.")
else:
print("\n UNATTRIBUTABLE WORLD: no root came back. On this machine the bridge will")
print(" NEVER terminate a launcher chain — it will stop after one route and say a")
print(" chain may remain. That is the designed behaviour, not a failure; it means")
print(" stall recovery is limited to reporting, and the user ends stray chains.")
return bool(attr and attr.get("pid"))
def route_table(launch):
"""Per route, FROM THE ROUTE. Every field here comes out of `routesTried`
and none of it out of the attempt: the attempt's copies are reset when the
next route begins, so reading them per row printed the last route's
evidence against every row — a table whose columns were all the same
number, which is not a measurement."""
print("\nPER ROUTE")
hdr = ("route", "invokedAt", "rootPid", "log", "runBegin", "cleanup")
print(" %-13s %-11s %-8s %-24s %-9s %s" % hdr)
for t in launch.get("routesTried", []):
if t.get("unattributedLog"):
log = "UNATTRIBUTED"
elif t.get("launcherLog"):
log = str(t["launcherLog"]).replace("/", "\\").rsplit("\\", 1)[-1]
else:
log = "-"
print(" %-13s %-11s %-8s %-24s %-9s %s" % (
t.get("route"),
t.get("invokedAt"),
t.get("rootPid") if t.get("rootPid") is not None else "-",
log[:24],
"yes" if t.get("runBeginSeen") else "no",
"%s%s%s" % (
t.get("cleanup"),
"" if t.get("rootConfirmed") else " [root unconfirmed]",
" [chain may remain]" if t.get("chainMayRemain") else "",
),
))
if t.get("note"):
print(" note: %s" % t["note"][:150])
hint = launch.get("lastRouteBeforeAttach")
if hint:
print(" attach happened while '%s' was in flight — ORDERING HINT ONLY (proof=%s)"
% (hint.get("route"), hint.get("proof")))
def main():
baseline_only = "--baseline" in sys.argv
if "--self-test" in sys.argv:
return self_test()
if "--probe-uia" in sys.argv:
optional_step("UIA reply capture", probe_find_control)
return 0
if "--probe-handle-launches" in sys.argv:
which_world()
return 0
print("SOLIDWORKS LAUNCH GATE")
print("=" * 72)
st = verb("swx_status")
version = st.get("bridgeVersion", "?")
print(f"bridge {version} mock={st.get('mock')} "
f"route={st.get('solidworks', {}).get('launchRoute')} "
f"preferred={st.get('launch', {}).get('preferredRoute')}")
if st.get("solidworks", {}).get("running"):
print("\nSOLIDWORKS IS ALREADY RUNNING. This gate must start from a closed one —")
print("run swx_close_all_safely (it preserves every document to recovery, verified, "
"before closing), or --baseline to look without touching anything.")
if not baseline_only:
return 2
base = probe("baseline before any launch")
base_chain = chain_pids(base)
base_guarded = pids(base, NEVER_TOUCH)
base_logs = log_names(base)
base_sld = pids(base, ("sldworks",))
print(f"\nBASELINE chain={sorted(base_chain)} daemons={sorted(base_guarded)} "
f"sldworks={sorted(base_sld)} launcherLogs={len(base_logs)}")
if baseline_only:
print(json.dumps(base, indent=1)[:4000])
return 0
# ── 1b. WHICH WORLD — asked BEFORE the run, because it decides how to
# read everything after it.
attributable = which_world()
print("\n (the launch started by the pre-check IS the one measured below)")
# ── 2. LAUNCH ──────────────────────────────────────────────────────────
t0 = time.time()
r = verb("swx_start", {
"reason": "launch-gate: proving the launch state machine end to end on this machine",
"waitSeconds": 0,
}, 90)
# The pre-check already started it, so this call must report rather than
# launch — which is itself one of the properties under test.
check(r.get("launchedNow") is not True,
"the second swx_start reported the in-flight launch instead of starting another",
json.dumps(r)[:200])
print(f"\nswx_start -> success={r.get('success')} stillLaunching={r.get('stillLaunching')} "
f"stage={r.get('stage')} route={r.get('route')}")
check(r.get("success") is True, "swx_start does not report a failure for a launch in flight",
json.dumps(r)[:300])
# ── 3. ONE CHAIN, and 4. RUN BEGIN ─────────────────────────────────────
new_chain, new_logs, run_begin_at, chain_seen_at = set(), set(), None, None
deadline = time.time() + RUN_BEGIN_BUDGET
while time.time() < deadline:
snap = probe("watching for the launcher chain and Run Begin")
c = chain_pids(snap) - base_chain
if c and chain_seen_at is None:
chain_seen_at, new_chain = time.time() - t0, c
new_chain |= c
nl = log_names(snap) - base_logs
new_logs |= nl
if run_begin(snap, new_logs):
run_begin_at = time.time() - t0
break
time.sleep(POLL)
check(len(new_chain) <= 2, "exactly ONE launcher chain was started (CATSTART + SWXDesktopLauncher)",
f"new pids: {sorted(new_chain)}")
check(len(new_logs) <= 1, "exactly ONE new launcher log appeared", f"{sorted(new_logs)}")
check(run_begin_at is not None,
f"the new launcher log reached 'Run Begin' within {RUN_BEGIN_BUDGET}s",
f"{run_begin_at:.1f}s" if run_begin_at else "never")
# ── 6. ATTACH ──────────────────────────────────────────────────────────
attached_at, sldworks_at = None, None
last_status = {}
deadline = time.time() + ATTACH_BUDGET
while time.time() < deadline:
s = verb("swx_status", {}, 40)
last_status = s
launch = s.get("launch", {})
if sldworks_at is None and s.get("solidworks", {}).get("processId"):
sldworks_at = time.time() - t0
if s.get("com", {}).get("running") or launch.get("stage") == "attached":
attached_at = time.time() - t0
break
if launch.get("finished") and launch.get("outcome") not in (None, "attached"):
print(f" ... the machine gave up: {launch.get('outcome')} — {launch.get('error')}")
break
print(f" ... stage={launch.get('stage')} route={launch.get('route')} "
f"{int(time.time() - t0)}s")
time.sleep(POLL)
verdict, detail = attach_verdict(attached_at, last_status)
if verdict == "signIn":
raise BlockedOnSignIn(detail)
check(attached_at is not None, f"COM attached within {ATTACH_BUDGET}s", detail)
# ── 6b. READY — attached is not ready ──────────────────────────────────
# MEASURED 2026-10-01: document commands sent 8 s after COM attach crashed
# SOLIDWORKS while its 3DEXPERIENCE add-in was still initialising. The bridge
# now reports `solidworks.ready` (StartupProcessCompleted + the performance
# log) and refuses document verbs until it is true; this gate waits for it,
# and probes NOTHING before it.
ready_at = None
if attached_at is not None:
deadline = time.time() + READY_BUDGET
while time.time() < deadline:
s = verb("swx_status", {}, 40)
sw = s.get("solidworks", {})
if sw.get("ready") is True:
ready_at = time.time() - t0
print(f" ready, based on {sw.get('readiness', {}).get('basedOn')}")
break
print(f" ... attached, not ready yet: {json.dumps(sw.get('readiness'))[:200]} "
f"{int(time.time() - t0)}s")
time.sleep(POLL)
check(ready_at is not None, f"SOLIDWORKS reported READY within {READY_BUDGET}s of the start",
f"{ready_at:.1f}s" if ready_at else "never")
# ── 5. IDEMPOTENCE — three calls, only once SOLIDWORKS is READY ─────────
# (Moved after READY on 2026-10-01: no probe of any kind reaches SOLIDWORKS
# before it has finished starting. In-flight idempotence of the launch
# machine itself is pinned by the suite, tests/launch.rs.)
before = probe("counting chains before the idempotence probes")
chains_before = chain_pids(before)
logs_before = log_names(before)
stages = []
for i in range(3):
again = verb("swx_start", {"reason": f"launch-gate: idempotence probe {i + 1}"}, 60)
stages.append((again.get("stage"), again.get("stillLaunching"), again.get("launchedNow")))
check(again.get("success") is True and again.get("launchedNow") is not True
and again.get("stage") == "ready",
f"swx_start after READY #{i + 1} launched nothing and reports stage ready",
json.dumps(stages[-1]))
time.sleep(1)
after = probe("counting chains after the idempotence probes")
if True:
check(chain_pids(after) - chains_before == set(),
"three swx_start calls after READY started ZERO new chains",
f"{sorted(chain_pids(after) - chains_before)}")
check(log_names(after) - logs_before == set(),
"...and produced ZERO new launcher logs",
f"{sorted(log_names(after) - logs_before)}")
# ── 7. NOTHING PRE-EXISTING WAS TOUCHED ────────────────────────────────
final = verb("swx_status", {}, 40).get("launch", {})
end = probe("final census: everything that was running before must still be")
if True:
alive = {int(p["pid"]) for p in end["procs"]}
check(base_guarded <= alive,
"every 3DEXPERIENCELauncher* daemon that was running is STILL running",
f"missing: {sorted(base_guarded - alive)}")
check(base_chain <= alive,
"every pre-existing launcher chain is STILL running",
f"missing: {sorted(base_chain - alive)}")
check(base_sld <= alive, "no pre-existing sldworks.exe was ended",
f"missing: {sorted(base_sld - alive)}")
# ── ROUND 2: the report must not hide a cleanup it could not perform ────
if final.get("cleanupFailed"):
check(bool(final.get("chainLeftRunning")),
"a cleanup that failed NAMES what it left running",
json.dumps(final.get("chainLeftRunning"))[:300])
check(final.get("crashRecovery") in ("off", "armed", "clicked"),
"the report says whether crash recovery is armed",
str(final.get("crashRecovery")))
# ── 8. THE REPORT ──────────────────────────────────────────────────────
print("\nTIMINGS")
for label, at in [("chain appeared", chain_seen_at), ("Run Begin", run_begin_at),
("sldworks.exe", sldworks_at), ("COM attach", attached_at), ("READY", ready_at)]:
print(f" {label:<16} {f'{at:6.1f}s' if at is not None else ' --'}")
print(f"\nROUTE won={final.get('route')} remembered={final.get('preferredRoute')}")
print(f" attributed={final.get('attributed')} rootConfirmed={final.get('rootConfirmed')} "
f"unattributedLog={final.get('unattributedLog')}")
route_table(final)
for t in final.get("routesTried", []):
print(f" tried {t.get('route'):<14} stage={t.get('stage'):<16} stalled={t.get('stalled')} "
f"killed={[k.get('pid') for k in t.get('killed', [])]}")
if attributable and final.get("attributed") is False:
NOTES.append("the pre-check saw a root but the finished attempt reports none — the route "
"that ran was not the one the pre-check probed")
optional_step("UIA reply capture", probe_find_control)
ledger = verb("swx_action_ledger", {"limit": 60}, 40)
for a in ledger.get("actions", []):
if str(a.get("action", "")).startswith("solidworks_launch") or "crash" in str(a.get("action", "")):
print(f" ledger {a.get('action')}: {json.dumps(a.get('detail'))[:160]}")
print("\n" + "=" * 72)
for n in NOTES:
print("NOTE: " + n)
if FAILURES:
print(f"GATE FAILED — {len(FAILURES)} claim(s) did not hold:")
for f in FAILURES:
print(" - " + f)
return 1
print("GATE PASSED — one chain, Run Begin, attach, READY, three no-op probes, nothing else touched.")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Unreadable as e:
# NOT a failure of the claim — a failure to ASK. Exit non-zero so it can
# never be mistaken for a pass, and say which probe went unanswered.
print("\n" + "=" * 72)
print("GATE ABORTED — a probe could not be read, so nothing below it is proven.")
print(" %s" % e)
print("This is 'could not ask', not 'nothing found'. Fix the probe and re-run.")
sys.exit(3)
except BlockedOnSignIn as e:
print("\n" + "=" * 72)
print("BLOCKED ON THE USER'S SIGN-IN — the launch is waiting for a person to sign in to "
"3DEXPERIENCE in the window %r." % str(e))
print("Nothing was killed and the launch's deadlines are paused. Only the user can answer "
"it; re-run the gate once they have signed in.")
sys.exit(4)