# Swing Control Bridge — Build & Proof Report

Date: 2026-07-04
Target: MCSJ.exe PID 4732 (C:\Edmunds\MCSJ26.1\MCSJ.exe), Edmunds/MCSJ 26.1
Workspace: C:/MCSJAgent/SwingBridge/

## Summary

Built a generic, MCSJ-agnostic in-process Swing introspection/control agent
(`SwingBridgeAgent.java`), attached it live into the running MCSJ JVM using
the JVM Attach API via `jattach`, and proved:
  1. Real-time TREE dump of MCSJ's actual live Swing component tree
     (read-only, per safety guardrails).
  2. CLICK / SETTEXT working end-to-end against a disposable throwaway
     Swing test app (never touched real MCSJ with a mutating command).

## IMPORTANT CORRECTION vs. task brief

The brief stated MCSJ.exe is a 64-bit JVM. **This is incorrect** — verified:

```
$ file /c/Edmunds/MCSJ26.1/MCSJ.exe
/c/Edmunds/MCSJ26.1/MCSJ.exe: PE32 executable for MS Windows 5.00 (GUI), Intel i386, 5 sections
```
and via PowerShell module inspection of the live process (PID 4732), it has
`wow64.dll`, `wow64win.dll`, `wow64cpu.dll` etc. loaded — the unmistakable
signature of a 32-bit process running under WOW64 on 64-bit Windows.
Confirmed empirically: the 64-bit `jattach.exe` refused with:

```
Cannot attach 64-bit process to 32-bit JVM
```

Fix: used the 32-bit `jattach32.exe` build (from jattach v2.1 release
assets — v2.2 no longer ships a 32-bit Windows binary) and attach succeeded
immediately. The bundled `jre_x64` directory next to MCSJ.exe exists but is
NOT what launch4j is actually running under this PID; MCSJ.exe itself is a
32-bit launch4j wrapper presumably using the 32-bit `jre` directory (also
present alongside `jre_x64`). This distinction matters for anyone attaching
tools to this process in future — always verify bitness first with `file`
or by checking for wow64*.dll modules, don't trust folder names.

## (a) Exact working commands to attach to a running MCSJ PID

```bash
# 1. Confirm live PID
tasklist //FI "IMAGENAME eq MCSJ.exe"

# 2. Attach the agent jar (MUST use the 32-bit jattach binary against MCSJ)
cd /c/MCSJAgent/SwingBridge
./jattach32.exe <PID> load instrument false "C:/MCSJAgent/SwingBridge/Agent.jar="

# Expected output on success:
#   Connected to remote process
#   Response code = 0
#   0

# 3. Verify the bridge is listening
python mcsj-swing-bridge-client.py ping
#   -> PONG

# 4. Read-only introspection (SAFE, always fine to run against live MCSJ)
python mcsj-swing-bridge-client.py tree --pretty
```

NOTE on re-attaching: the agent class has a static "already started" guard,
so re-loading the exact same `Agent.jar` (same class name) into a JVM that
already has it loaded is a safe no-op (idempotent) as long as the listener
previously bound successfully. If the listener thread died (e.g. port
already in use by something else at load time), you must compile a fresh
copy of the agent under a **new class name** (e.g. `SwingBridgeAgentV2`,
included in this repo as `AgentV2.jar`) to get a clean restart, since Java
does not let you unload/reload a class with the same name once JIT'd into
a running JVM via the Attach API without a full class-loader trick. This is
documented here because it happened once during this session (see Blockers).

jattach syntax notes (from `jattach32.exe` / `jattach.exe` with no args):
```
Usage: jattach <pid> <cmd> [args ...]
Commands:
    load  threaddump   dumpheap  setflag    properties
    jcmd  inspectheap  datadump  printflag  agentProperties
```
For `load`, the argument form is: `load <agent-lib-name-or-path> <true|false-for-onload-vs-agentmain-style> "<agent-jar>=<agent-args>"`.
In practice for a plain instrumentation agent jar with `Agent-Class` in its
manifest (as opposed to a native agent lib), the working invocation is:
```
jattach32.exe <pid> load instrument false "C:/path/to/Agent.jar="
```
(trailing `=` is required even with empty args — it separates the jar path from agent-args).

## (b) REAL sample of TREE output captured from the LIVE MCSJ JVM

Captured live via:
```bash
python mcsj-swing-bridge-client.py tree > mcsj-tree-live-capture.json
```
Full capture saved at `C:/MCSJAgent/SwingBridge/mcsj-tree-live-capture.json`
(5,861 bytes). Key excerpt proving this is REAL MCSJ, not a fake/decoy:

```json
{"windowCount":1,"windows":[
  {"class":"core.client.McsClient","name":"frame0",
   "text":"MCSJ - 2026.1 (chestervillagenydb)",
   "bounds":{"x":678,"y":0,"w":1249,"h":1037},
   "visible":true,"enabled":true,"showing":true,
   "title":"MCSJ - 2026.1 (chestervillagenydb)",
   "children":[
     ...
     {"class":"misc.components.fx.EFXInternalFrame","name":"MAINTENANCE", ...
       children include: "ap.client.fx.EFXPomastPanel"
     ...
     {"class":"misc.components.EMenuBar", ... "children":[
        {"class":"misc.components.EMenu","text":"File", ...},
        {"class":"misc.components.EMenu","text":"Finance", ...},
        {"class":"misc.components.EMenu","text":"Billing/Collections", ...},
        {"class":"misc.components.EMenu","text":"Personnel", ...},
        {"class":"misc.components.EMenu","text":"System Utilities", ...},
        {"class":"misc.components.EMenu","text":"Window", ...},
        {"class":"misc.components.fx.EFXFavoritesMenu","text":"Favorites", ...},
        {"class":"misc.components.EMenu","text":"Help", ...},
        {"class":"javax.swing.JLabel","text":"    Search: ", ...},
        {"class":"misc.components.ESearchBar", ...}
     ]}
  ]}
]}
```

This unambiguously proves the agent is inside the real MCSJ JVM: real
window title `MCSJ - 2026.1 (chestervillagenydb)` (the actual demo DB
name), the real MCSJ menu bar text (File / Finance / Billing-Collections /
Personnel / System Utilities / Window / Favorites / Help / Search), an
open "MAINTENANCE" internal frame, and genuine internal MCSJ/Edmunds
package/class names (`core.client.McsClient`, `misc.components.EMenu`,
`misc.components.fx.EFXFavoritesMenu`, `ap.client.fx.EFXPomastPanel`,
`misc.components.ESearchBar`) that could not come from any generic Swing
demo — these are Edmunds' own proprietary widget classes.

No CLICK or SETTEXT command was ever sent to this live process — only
`PING` and `TREE`, both strictly read-only, per the safety guardrails.

## (c) CLICK / SETTEXT proof against the disposable test app

Disposable app: `src/TestApp.java` -> compiled/packaged as `TestApp.jar`,
launched as its own separate `java.exe` process (PID 4416 in this run,
NOT related to MCSJ), with a JButton, JTextField, and JLabel, each wired
to print to stdout on interaction.

Commands run:
```bash
python mcsj-swing-bridge-client.py click MyClickButton
python mcsj-swing-bridge-client.py settext MyTextField hello world from agent
python mcsj-swing-bridge-client.py gettext MyTextField
python mcsj-swing-bridge-client.py gettext StatusLabel
```

Client-side responses:
```
OK class=javax.swing.JButton
OK class=javax.swing.JTextField
OK class=javax.swing.JTextField text=hello world from agent
OK class=javax.swing.JLabel text=status: clicked!
```

TestApp's own console output (`testapp.log`), proving the click/settext
genuinely fired real Swing listeners inside the target JVM (not just
returning a canned "OK"):
```
[TestApp] window shown, ready.
[SwingBridgeAgent] listening on 127.0.0.1:9797
[TestApp] BUTTON CLICKED. current field text = 'initial-value'
[TestApp] FIELD TEXT CHANGED to = ''
[TestApp] FIELD TEXT CHANGED to = 'hello world from agent'
```

A follow-up TREE capture (before this jar's listener was torn down)
confirmed the field text and label text updated live inside the JSON tree
snapshot too (`"text":"hello world from agent"` / `"text":"status: clicked!"`).

## (d) Blockers hit

1. **Bitness mismatch (real blocker, worked around).** MCSJ.exe is a
   32-bit process (WOW64), not 64-bit as assumed in the task brief. The
   64-bit `jattach.exe` (from jattach v2.2, the latest release) failed
   with the exact error:
   ```
   Cannot attach 64-bit process to 32-bit JVM
   ```
   Resolved by downloading `jattach32.exe` from the jattach v2.1 GitHub
   release assets (v2.2 dropped the 32-bit Windows binary). Both
   `jattach.exe` (64-bit, v2.2) and `jattach32.exe` (32-bit, v2.1) are
   kept in this directory; **use the 32-bit one for MCSJ**, and note any
   other target 32-bit JVM on this box needs the same.

2. **No integrity-level / UAC / access-denied blocker was encountered.**
   Both jattach binaries were run as the same user session as MCSJ (not
   elevated), and attach succeeded on the first try once bitness was
   fixed. No evidence was seen of High Integrity blocking the Attach API
   itself (this is a different code path from SendInput/PostMessage —
   the JVM Attach API uses a named-pipe/socket-file handshake in
   `%TEMP%\.java_pid<pid>` plus an OS signal/APC style trigger, which
   apparently is not covered by the same UIPI message-filtering that
   blocks window messages). If elevation had been required, the plan was
   to re-run jattach32.exe from an elevated shell — this was not needed.

3. **Static "already started" flag caused a one-time restart snag
   (self-inflicted, not an environment blocker).** During testing, the
   first `Agent.jar` load into MCSJ happened while port 9797 was still
   held by the (then-current) disposable TestApp process on the same
   box, so the MCSJ-side listener thread in that first load attempt
   never got to bind and the class's `STARTED` AtomicBoolean was already
   flipped true, preventing a clean retry on the same class name after
   the port was freed. Fixed by compiling a second copy of the identical
   agent logic under a fresh class name (`SwingBridgeAgentV2`, packaged
   as `AgentV2.jar`) and re-attaching that — succeeded immediately. For
   future repeated attach/detach cycles against the same long-lived JVM,
   either (a) always free the port before the first attach, or (b) keep
   a small pool of pre-compiled agent jars under different class names
   for retries, or (c) add a "rebind on demand" admin command to the
   protocol (not implemented here, out of scope for this proof).

No other blockers. JDK 8 (Temurin 8u492) and jattach both downloaded
directly from public internet URLs without any proxy/firewall issue.

## (e) Client script and how to invoke it going forward

Path: `C:/MCSJAgent/SwingBridge/mcsj-swing-bridge-client.py`

Usage:
```bash
# Read-only introspection (always safe against live MCSJ):
python mcsj-swing-bridge-client.py tree --pretty
python mcsj-swing-bridge-client.py gettext "<selector>"
python mcsj-swing-bridge-client.py ping

# Mutating commands (ONLY ever exercised against the disposable TestApp
# in this session; use with real caution/authorization against live MCSJ):
python mcsj-swing-bridge-client.py click "<selector>"
python mcsj-swing-bridge-client.py settext "<selector>" <value...>

# Raw protocol passthrough:
python mcsj-swing-bridge-client.py raw "TREE"

# Point at a non-default host/port if the agent was started with a custom port arg:
python mcsj-swing-bridge-client.py --port 9797 tree
```

Selector matching rule (in `SwingBridgeAgent.findComponent`): first exact
match (case-sensitive) on `getName()` OR visible text, across a pre-order
walk of all `Window.getWindows()` and their descendants; falls back to a
case-insensitive pass if no exact match is found. First match wins.

## Files delivered (all under C:/MCSJAgent/SwingBridge/)

```
src/SwingBridgeAgent.java     — the generic agent source (agentmain/premain, TCP server, TREE/CLICK/SETTEXT/GETTEXT)
src/SwingBridgeAgentV2.java   — identical logic, distinct class name (used for the clean re-attach, see Blocker #3)
src/TestApp.java              — disposable throwaway Swing test app (JButton/JTextField/JLabel) used to prove CLICK/SETTEXT
MANIFEST.MF                   — Agent-Class: SwingBridgeAgent, Can-Redefine/Retransform-Classes: false
MANIFEST-V2.MF                — same, Agent-Class: SwingBridgeAgentV2
TestApp-MANIFEST.MF           — Main-Class: TestApp
Agent.jar                     — compiled+packaged agent (use this for MCSJ)
AgentV2.jar                   — compiled+packaged agent, alternate class name (fallback if Agent.jar's listener is already bound/dead)
TestApp.jar                   — compiled+packaged disposable test app
jattach.exe                   — jattach v2.2, 64-bit Windows build (does NOT work against MCSJ — 32-bit JVM)
jattach32.exe                 — jattach v2.1, 32-bit Windows build (WORKS against MCSJ)
jdk8-extracted/jdk8u492-b09/  — portable Eclipse Temurin JDK 8u492 (javac/jar/java), used to build everything, JRE-only box otherwise
build/                        — raw compiled .class files (agent + test app)
mcsj-swing-bridge-client.py   — Python CLI client (tree/click/settext/gettext/ping/raw)
mcsj-tree-live-capture.json   — REAL captured TREE output from live MCSJ PID 4732 (the proof artifact)
testapp.log                   — console output from the disposable TestApp proving CLICK/SETTEXT landed on real listeners
REPORT.md                     — this file
```

---

## APPENDIX: JavaFX Bridge (FXTREE/FXCLICK/FXSETTEXT/FXGETTEXT) — added 2026-07-04, independently verified by Michel

The Swing-only bridge above cannot see MCSJ's embedded JavaFX panels (JFXPanel), which is where modern screens like Utility Account Maintenance actually live. A second agent (SwingBridgeAgentV4, `AgentV4.jar`) adds JavaFX scene-graph support using the same jattach injection pattern, on the JavaFX Application Thread (Platform.runLater), not the Swing EDT.

### New commands
```
FXTREE                          -> full JSON dump of every JFXPanel's live JavaFX scene graph
FXCLICK <selector>               -> ButtonBase.fire(), Tab selection via TabPane, or TableView.selectFirst()
FXSETTEXT <selector> <value>    -> TextInputControl.setText()
FXGETTEXT <selector>            -> current text
```
Same attach syntax as the Swing agent, distinct port to avoid collision with an already-bound agent:
```bash
./jattach32.exe <pid> load instrument false "C:/MCSJAgent/SwingBridge/AgentV4.jar=<port>"
```

### Bug found and fixed during independent verification (not caught by the original build)
Initial `FXCLICK` on a `Tab` and `fire()` on a `Button` threw:
```
java.lang.IllegalAccessException: Class SwingBridgeAgentV3 can not access a member of class
javafx.scene.control.TabPane$TabPaneSelectionModel with modifiers "public"
```
Root cause: JavaFX's concrete selection-model/skin classes are package-private even though their methods are public — a classic Java reflection gotcha. Fixed by calling `method.setAccessible(true)` before `invoke()` on all three FX mutation paths (fire, tab-select, table selectFirst, setText). Rebuilt as `SwingBridgeAgentV4` / `AgentV4.jar`.

### Verified proof (re-run independently after the fix, against a fresh disposable FxTestApp process)
```
FXTREE          -> full real scene graph incl. MyFxClickButton, MyFxTextField, MyFxTabPane (TabOne/TabTwo)
FXCLICK MyFxClickButton  -> OK class=javafx.scene.control.Button method=fire()
FXCLICK TabTwo           -> OK class=javafx.scene.control.Tab method=TabPane.getSelectionModel().select(tab)
FXSETTEXT MyFxTextField hello_verified_by_michel -> OK
FXGETTEXT MyFxTextField -> OK class=javafx.scene.control.TextField text=hello_verified_by_michel
```
Follow-up FXTREE confirmed via independent read-back: `"selectedTabText":"TabTwo"`, `"selected":true` — not just a claimed "OK" response, the state genuinely changed.

### Live MCSJ proof (real production-adjacent screen, read-only FXTREE only)
Injected `AgentV4.jar` into the live MCSJ process (PID 4732) on port 9801, alongside the existing Swing bridge (still healthy on 9797). `FXTREE` returned **2.3MB of real live JavaFX scene graph** from the actual open screens:

- **Utility Account Maintenance** (account 601-2, CHESTER ACQUISITIONS LLC, 115 Main St, Type RES) — real fields captured: Account Id, Type, Section, Prop Loc/Serv Loc/City Id, Location Id, SBL/Block/Lot/Qualifier, Owner, Bill To, Alternate Id, and Balances screen structure: `Total Balances` / `Water` / `Sewer` tab-like labels, `Totals for Water`, `Totals for Sewer`, `Totals for SPRINKLERS`, `Totals for BACKFLOW` titled panes, Principal Balance / Total Balance columns, Transfer Balance button.
- A second open screen, a real **Purchase Order** (PO 26-02126, vendor AAA EMERGENCY SUPPLY CO INC, "Road flares and barricades", Open status) — confirms the bridge generalizes across MCSJ modules, not just Utility.
- Real toolbar buttons captured with mnemonic prefixes as MCSJ defines them: `_Add`, `_Edit`, `_Close`, `Delete`, `_Previous`, `_Next`, `_Detail`, `_Letter`, `View _Map`, `_Help`, `_Save`, `_Cancel`, `_Print`, `_Line Item`.

**RESOLVED 2026-07-04, verified live** (superseding the "not attempted" note below — that was written before actually testing it): MCSJ's "Total Balances/Water/Sewer" balance-view switcher IS a real `misc.components.fx.EFXTabPane` (an MCSJ subclass of `javafx.scene.control.TabPane`), correctly matched by `isInstanceOfFx` (which follows Java subclass semantics) and correctly enumerated by the agent's existing `collectFx` logic (which walks `TabPane.getTabs()` separately since `Tab` isn't a `Node`). Live test against real MCSJ account 601-2: screen was showing the `Sewer` sub-tab (confirmed via screenshot), `FXCLICK "Total Balances"` returned `OK class=javafx.scene.control.Tab method=TabPane.getSelectionModel().select(tab)`, and a follow-up screenshot confirmed the tab genuinely switched — real Water $320.66 / Sewer $0.00 / Total $320.66 balance data rendered, along with Deposit Balance, Interest, Transfer Balance, and Calculate Installment Plan controls. **FXCLICK on Tab objects is fully reliable here — no coordinate fallback needed for balance sub-tab switching.**

### Files
- `src/SwingBridgeAgentV4.java`, `AgentV4.jar`, `MANIFEST-V4.MF` (fixed version — use this, not V3)
- `mcsj-fxtree-live.json` — the 2.3MB real live capture referenced above
