Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMFrob.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ add_executable (
${TADS2_HEADERS}
${TADS3_HEADERS}
src/osportable.cc
src/debuguifactory_nodebug.cc
tads3/vmrun.cpp
$<TARGET_OBJECTS:FROB_OBJECTS>
$<TARGET_OBJECTS:COMMON_OBJECTS>
Expand Down Expand Up @@ -154,6 +155,11 @@ if (ENABLE_FROBD)
${TADS3_HEADERS}
src/osportable.cc
src/debugui.cc
src/dap/dapdebugui.cc
src/dap/dap_framing.cc
src/dap/dapdebugui_io.cc
src/frobdebughelper.cc
src/debuguifactory.cc
tads3/vmdbg.cpp
tads3/vmrun.cpp
tads3/vmbt3_d.cpp
Expand Down
16 changes: 14 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.1)
cmake_minimum_required(VERSION 3.10)
project(FrobTADS)

option (
Expand Down Expand Up @@ -46,7 +46,7 @@ option (
option (
ENABLE_FROBD
"Build frobd, a version of frob usable by debuggers."
OFF
ON
)

if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.13")
Expand Down Expand Up @@ -221,3 +221,15 @@ configure_file (
${PROJECT_SOURCE_DIR}/frob_config.h.in
${PROJECT_BINARY_DIR}/frob_config.h
)

option (
ENABLE_TESTS
"Build the unit tests."
OFF
)

if (ENABLE_TESTS)
include(CTest)
enable_testing()
add_subdirectory(tests)
endif()
152 changes: 152 additions & 0 deletions doc/SOCKET_DAP_DOCUMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# DAP Transport: stdio, Unix socket, TCP

This document is the canonical reference for running `frobd` in DAP mode over stdio, a Unix domain socket, or a TCP socket.

## Overview

`frobd` supports three DAP communication modes:

1. **stdio** (default) - DAP over stdin/stdout
2. **Unix domain socket** - DAP over a local socket file
3. **TCP socket** - DAP over a TCP listener (can be local or remote)

Using sockets instead of stdin/stdout keeps the DAP protocol on a separate channel, which avoids conflicts with interactive terminal input.

## Command-line options

### Common flags

- `-D dap` / `--debug-protocol dap`: enable DAP mode
- `-i plain` / `--interface plain`: recommended for IDE/integration usage (avoids curses UI interactions)

### Unix domain socket

- `-Q <path>` / `--dap-socket <path>`: Unix domain socket path
- Default: `/tmp/tads-dap.sock`

Example:

```bash
./build/frobd -i plain -D dap --dap-socket /tmp/tads-dap.sock /path/to/game.t3
```

Behavior:

- Creates (and overwrites) the socket file at the given path
- Listens and waits until a DAP client connects
- Runs all DAP traffic over the accepted socket connection

### TCP socket

- `-P <port>` / `--dap-port <port>`: TCP port to listen on

Example:

```bash
./build/frobd -i plain -D dap --dap-port 9876 /path/to/game.t3
```

Behavior:

- Listens on the given port
- Binds to all interfaces by default (`INADDR_ANY`), which enables remote connections

If you enable TCP mode, treat it as a remote-debug interface (choose a safe port, and consider firewalling).

### stdio (legacy / reference)

```bash
./build/frobd -i plain -D dap /path/to/game.t3
```

This uses stdin/stdout for DAP, which can interfere with interactive terminal UI and user input.

## VS Code configuration

The VS Code extension uses the `dapMode`/`dapSocket`/`dapPort` launch configuration keys.

Example `launch.json`:

```json
{
"type": "tads3",
"request": "launch",
"name": "Debug TADS3 (DAP socket)",
"program": "${workspaceFolder}/game.t3",

"frobd": "/path/to/frobd",

"dapMode": "socket",
"dapSocket": "/tmp/tads-dap.sock"
}
```

TCP mode example:

```json
{
"type": "tads3",
"request": "launch",
"name": "Debug TADS3 (DAP TCP)",
"program": "${workspaceFolder}/game.t3",

"frobd": "/path/to/frobd",

"dapMode": "tcp",
"dapPort": 9876
}
```

## Implementation notes (frobtads_debugger)

- `src/main.cc`: CLI flags (`-P/--dap-port`, `-Q/--dap-socket`)
- `src/debuguifactory.cc`: selects stdio vs socket vs TCP DAP transport
- `src/dap/dapdebugui_io.cc`: transport setup and fd-backed read/write

### Breakpoint registry synchronization

When the DAP `setBreakpoints` request toggles breakpoints, successful changes are mirrored into the shared helper breakpoint registry.

This keeps breakpoint-related “read views” (for example: terminal breakpoint listings and source-print marker lookup) consistent with breakpoints set or removed via the DAP path.

## Testing (Python)

Two Python integration tests exercise DAP over Unix sockets and over TCP.

Prerequisites:

- A built `frobd` binary (commonly `./build/frobd`)
- A compiled TADS3 image file (`.t3`) to run (any small game/program is fine)
- Python 3

Unix domain socket test:

```bash
python3 tests/dap_integration_test_socket.py ./build/frobd /path/to/game.t3
```

TCP test:

```bash
python3 tests/dap_integration_test_tcp.py ./build/frobd /path/to/game.t3
```

Notes:

- The socket test creates a temporary socket path and starts `frobd` with `--dap-socket <temp>/frobd_dap.sock`.
- The TCP test auto-selects a free port and starts `frobd` with `-P <port>`.
- Set `NO_COLOR=1` if you want non-colored output.

## Testing (manual smoke checks)

If you only want to verify that the socket/port is reachable (not a full DAP exchange), you can connect with `nc`:

```bash
# Unix domain socket
nc -U /tmp/tads-dap.sock

# TCP
nc 127.0.0.1 9876
```

For actual DAP correctness, prefer the Python integration tests above.
90 changes: 90 additions & 0 deletions src/dap/dap_framing.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "dap/dap_framing.h"

#include "common.h"
#include <cerrno>
#include <cstdlib>

namespace dap {

std::string make_content_length_header(std::size_t content_length) {
return "Content-Length: " + std::to_string(content_length) + "\r\n\r\n";
}

std::string frame_json_message(const json &message) {
const std::string payload = message.dump();
return make_content_length_header(payload.size()) + payload;
}

static bool try_parse_size(const std::string &text, std::size_t &value) {
errno = 0;
char *end = nullptr;
const unsigned long long parsed = std::strtoull(text.c_str(), &end, 10);
if (errno != 0 || end == text.c_str() || end == nullptr) {
return false;
}
// Reject trailing garbage.
while (*end != '\0') {
if (*end != ' ' && *end != '\t') {
return false;
}
++end;
}
value = static_cast<std::size_t>(parsed);
return true;
}

bool try_parse_content_length_line(const std::string &line,
std::size_t &content_length) {
constexpr const char *kPrefix = "Content-Length:";
if (line.rfind(kPrefix, 0) != 0) {
return false;
}

std::string rest = line.substr(std::char_traits<char>::length(kPrefix));
// Optional whitespace after ':'
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t')) {
rest.erase(rest.begin());
}

std::size_t parsed = 0;
if (!try_parse_size(rest, parsed)) {
return false;
}

content_length = parsed;
return true;
}

bool parse_framed_json_message(const std::string &framed, json &out) {
// Must start with the Content-Length header.
std::size_t header_end = framed.find("\r\n\r\n");
if (header_end == std::string::npos) {
return false;
}

// Only handle a single Content-Length header line here.
// If there are multiple headers, the production reader handles them; for unit
// tests we keep this strict.
const std::string header_line = framed.substr(0, framed.find("\r\n"));

std::size_t content_length = 0;
if (!try_parse_content_length_line(header_line, content_length)) {
return false;
}

const std::size_t body_start = header_end + 4;
if (framed.size() < body_start + content_length) {
return false;
}

const std::string body = framed.substr(body_start, content_length);

try {
out = json::parse(body);
return true;
} catch (...) {
return false;
}
}

} // namespace dap
34 changes: 34 additions & 0 deletions src/dap/dap_framing.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#ifndef DAP_FRAMING_H
#define DAP_FRAMING_H

#include "common.h"
#include <cstddef>
#include <string>

#include "json.hpp"

namespace dap {

using json = nlohmann::json;

// Formats a DAP/JSON-RPC style header for a given content length.
// Example: "Content-Length: 123\r\n\r\n"
std::string make_content_length_header(std::size_t content_length);

// Frames a JSON message using the DAP transport framing.
// Result: header + JSON payload.
std::string frame_json_message(const json &message);

// Attempts to parse a single framed message string into JSON.
// Returns false on framing or JSON parse errors.
bool parse_framed_json_message(const std::string &framed, json &out);

// Attempts to parse a Content-Length header line.
// Accepts: "Content-Length: <n>" (with optional spaces after ':').
// Returns true only if the line is a valid Content-Length header.
bool try_parse_content_length_line(const std::string &line,
std::size_t &content_length);

} // namespace dap

#endif /* DAP_FRAMING_H */
Loading