Skip to content

Release v0.2.2#95

Merged
thiagoralves merged 18 commits into
mainfrom
development
Apr 14, 2026
Merged

Release v0.2.2#95
thiagoralves merged 18 commits into
mainfrom
development

Conversation

@thiagoralves
Copy link
Copy Markdown
Contributor

Summary

Merge development into main for v0.2.2 release.

See PR #94 for full changelog: variable forcing via GDB field writes, Set Value interception, REPL refactoring, debugger timer fix.

🤖 Generated with Claude Code

thiagoralves and others added 18 commits April 2, 2026 14:19
Replace the broken GDB expression evaluation path for variable forcing
with a cross-platform IPC command pipe. The debug binary now accepts
a --cmd-pipe argument to start a command server thread that listens for
REPL commands from the VSCode extension.

Architecture:
- Extract process_command() from the REPL loop into a shared function
  that both the interactive REPL and the command server call — single
  source of truth for force/unforce/get/set commands.
- New iec_command_server.hpp: listener thread using Unix domain socket
  (Linux/macOS) or Win32 Named Pipe (Windows), newline-delimited text
  protocol with the same command format as the interactive REPL.
- New repl-client.ts: TypeScript IPC client using Node.js net module
  (handles both Unix sockets and Win32 named pipes transparently).
- Force/unforce commands now work while the program is running, not
  just when paused at a breakpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The resolveDebugConfiguration() path in debug-config-provider.ts was
not passing cmdPipePath to buildDebugConfig(), so the debug binary
was launched without --cmd-pipe and the command server never started.
Also increase the connect delay to 1s and show a warning toast if
the connection fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add diagnostic logging to trace exactly where the pipe connection
breaks. Logs go to the "STruC++ Debug" output channel and console:

- commands.ts: logs generated pipe path
- debug-config-provider.ts: logs debugState.cmdPipePath
- debug-config-builder.ts: logs args passed to binary
- extension.ts: logs session config, connect attempt, success/failure
- force-variable.ts: logs replClient state when force is invoked

Check Output > "STruC++ Debug" panel after launching a debug session
to see the full trace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lient

Two bugs in the IPC connection:

1. ReplClient: net.Socket.setTimeout(5000) fires on idle data, not just
   during connection. Once connected, the socket gets destroyed after 5s
   of no commands. Fix: disable timeout after successful connection with
   sock.setTimeout(0).

2. CommandServer: handle_client() read() blocks indefinitely if client
   disconnects without closing cleanly. Fix: set SO_RCVTIMEO on client
   socket so read() returns EAGAIN on timeout, allowing the loop to
   check running_ and accept new connections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ence

Log each retry attempt with whether the socket file exists on disk.
This will reveal whether the binary hasn't created the socket yet,
or whether the socket exists but connection fails.

Also increase retries to 8 and base delay to 250ms for more total
wait time (~64s max).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GDB's all-stop mode freezes all threads (including the command server)
when any thread hits a breakpoint. Connecting at session start races
with GDB pausing the binary during startup, causing ECONNREFUSED.

Fix: defer the connection to the first force/unforce command via
ensureConnected(). By the time the user right-clicks a variable, the
program has been running long enough for the command server to be
active.

If the program is paused at a breakpoint when the user tries to force,
the connection will fail because GDB has frozen the server thread.
The error message now tells the user to resume execution first.

Also found: cppdbg provides evaluateName as GDB cast expressions like
((strucpp::TYPE *)0xaddr)->VAR, not clean paths. This needs separate
handling (tracked for follow-up).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the command server IPC approach with direct GDB memory writes
for variable forcing. When stopped at a breakpoint, the extension
sets the IECVar internal fields directly via DAP evaluate:

  evaluateName.forced_ = true
  evaluateName.forced_value_ = <value>
  evaluateName.value_ = <value>

GDB ignores C++ access specifiers and can write private struct fields.
This works reliably when stopped at a breakpoint — the primary use
case since the Variables pane is only populated when stopped.

The command server (iec_command_server.hpp) and ReplClient are kept
in the codebase for future use (Option D hybrid: GDB when stopped,
pipe when running), but the force commands no longer depend on them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DAP evaluate request requires a frameId to specify which stack
frame the expression should be evaluated in. Without it, cppdbg
returns "Cannot evaluate expression on the specified stack frame".

Fix: before evaluating, fetch the active thread's topmost stack
frame and pass its ID to the evaluate request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The debug adapter tracker transforms evaluate requests with context
"repl" or "watch" by uppercasing identifiers (ST is case-insensitive).
This breaks force field writes because C++ namespaces are case-
sensitive: strucpp:: becomes STRUCPP:: which LLDB can't resolve.

Fix: use context "variables" for force/unforce evaluate requests.
The tracker only transforms "repl" and "watch" contexts, so
"variables" passes through untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove console.log statements and verify read-back that were added
during debugging. The force mechanism is confirmed working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When GDB pauses the program at a breakpoint, wall clock time advances
while the cyclic loop is frozen. On resume, the sleep_until target
(next_tick) is in the past, so the loop runs at full speed without
sleeping — executing hundreds of cycles instantly. This causes timers
(TON, TOF, TP) to fire almost immediately instead of respecting their
configured delay.

Fix: after each cycle, check if next_tick has fallen behind the
current wall clock. If so, reset it to now. This prevents catch-up
bursts while still maintaining accurate timing during normal execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for the debug variable interaction:

1. After forcing a variable, issue a dummy evaluate in "repl" context
   to trigger cppdbg's variable cache invalidation. This makes the
   forced value visible immediately in the Variables pane without
   needing to step.

2. Intercept setExpression DAP requests (triggered by "Set Value" in
   the Watch pane) and rewrite them as evaluate requests that assign
   to .value_ directly. This avoids the "expression could not be
   evaluated" error caused by cppdbg trying to assign to the IECVar
   wrapper type.

3. Note: "Set Value" in the Variables pane sends setVariable (not
   setExpression). This is harder to intercept since it uses
   variablesReference IDs rather than expression paths. For now,
   users should use "STruC++: Force Variable" from the context menu
   for the Variables pane.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log every request passing through the debug adapter tracker to see
exactly what VSCode sends for Set Value (setVariable vs setExpression),
and what the refresh evaluate returns after forcing.

Check the "STruC++ Debug" output channel for [tracker] lines and
Developer Tools Console for [strucpp:force] lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes based on DAP trace analysis:

1. Variable refresh after force: the "repl" context evaluate failed
   because it had no frameId. Replace with a read-back of the forced
   value via debugEvaluate() which always includes a frameId. This
   also uses "variables" context to avoid the ST uppercasing.

2. setExpression rewrite (Watch pane "Set Value"): the expression
   wasn't being uppercased, so LLDB couldn't find the ST variable
   name. Now applies transformStExpression() before rewriting.

3. setVariable (Variables pane "Set Value"): logged but left to
   cppdbg — it uses variablesReference IDs which we can't map to
   evaluateNames without caching the variable tree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Variables pane "Set Value" sends a setVariable DAP request with
only variablesReference + name (no evaluateName). To rewrite this
as an evaluate that sets .value_ directly, cache the evaluateName
for each variable when the variables response passes through.

Implementation:
- Track pending variables requests (seq → variablesReference)
- On variables response, cache (variablesReference, name) → evaluateName
- On setVariable, look up the cached evaluateName and rewrite to
  an evaluate request: evaluateName.value_ = <value>
- Cache lastFrameId from stackTrace responses to provide frame context

Also applies to setExpression (Watch pane Set Value) which now
correctly uppercases the ST expression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ging

1. Remove duplicate JSDoc comment on varEvalNameCache.

2. Remove debug logging: per-request DAP log in tracker, console.log
   in debug-config-builder and debug-config-provider.

3. Clear varEvalNameCache and pendingVarRequests on DAP "continued"
   event (when execution resumes, cached references become stale).

4. Remove dead IPC infrastructure that was built for Option D (pipe
   when running) but never used — forcing only works via GDB when
   stopped at a breakpoint:
   - Delete iec_command_server.hpp (C++ socket server)
   - Delete repl-client.ts (TypeScript socket client)
   - Remove --cmd-pipe arg parsing from generated main()
   - Remove cmdPipePath from DebugBuildState and DebugBuildInfo
   - Remove __cmdPipePath from debug configurations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rcing

feat: variable forcing via GDB field writes, IPC command server, timer fix
@thiagoralves thiagoralves merged commit cbd67e3 into main Apr 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant