Why I Started Using This Tool

build and deploy finish on their own, so VS Code knows they're done when they exit. gdbserver never exits on its own. It sits there waiting for the debugger. If VS Code waited for it to exit, it would wait forever. If it started the debugger straight away, it could connect before gdbserver is listening and get refused. This is the part of tasks.json that confused me most, so I'm taking it slowly.

What It Does

{
  "label": "gdbserver",
  "type": "shell",
  "command": "ssh",
  "args": [
    "-o", "StrictHostKeyChecking=accept-new",
    "${config:user}@${config:host}",
    "chmod +x ${config:remoteDir}/${config:bin} && exec gdbserver --once :${config:gdbPort} ${config:remoteDir}/${config:bin}"
  ],
  "isBackground": true,
  "dependsOn": ["deploy"],
  "problemMatcher": {
    "owner": "gdbserver",
    "pattern": {
      "regexp": "^(__gdbserver_never_matches__)$",
      "file": 1,
      "location": 1,
      "message": 1
    },
    "background": {
      "activeOnStart": true,
      "beginsPattern": ".",
      "endsPattern": "Listening on port"
    }
  }
}

ssh user@host "some command" logs into the board, runs that one command there, and shows its output. The command has two parts joined by &&, which means "run the second part only if the first succeeded":

  • chmod +x /home/root/main makes the file executable. Windows files don't have Linux's "executable" permission, so the file usually arrives from scp without it. Skip this and you get Permission denied.
  • exec gdbserver --once :2345 /home/root/main:
    • gdbserver is a small debugging helper that runs on the board. It starts your program, pauses it before the first line of main, and waits for a full GDB on your PC to connect over the network. The heavy debugger runs on the PC; only this small stub runs on the board. If you get gdbserver: not found, check with which gdbserver on the board.
    • :2345 is the port, from gdbPort. The empty part before the colon means "accept connections on any of the board's network addresses".
    • --once makes gdbserver exit after the first debug session instead of waiting for another, so each run starts clean and no old gdbserver holds the port.
    • exec replaces the shell with gdbserver, so when VS Code stops the task and the SSH connection closes, gdbserver goes away too instead of being left running on the board.

The background problem matcher

"isBackground": true tells VS Code: "this task keeps running. Don't wait for it to exit. Watch its output, and I'll tell you what 'ready' looks like." The background block describes "ready":

  • activeOnStart: true: the task counts as busy as soon as it starts.
  • beginsPattern: ".": . matches any character, so any line of output counts as "started working".
  • endsPattern: "Listening on port": when gdbserver is ready, it prints:
    Process /home/root/main created; pid = 812
    Listening on port 2345
    When VS Code sees Listening on port, the task is marked ready, and anything waiting on it (like the debugger's preLaunchTask) can continue.

So what's the odd pattern for? VS Code only accepts a background matcher inside a full problem matcher, and every problem matcher needs a pattern for finding errors. I don't want gdbserver's output turned into "problems", so the regex is ^(__gdbserver_never_matches__)$, a line that will never appear. file, location, and message are required, so they all point at capture group 1, but they're never used. It's a placeholder that satisfies the schema and does nothing. owner is just a name that groups any problems this matcher reports.

Running the whole chain

  • Ctrl+Shift+B runs just build.
  • Ctrl+Shift+P → Tasks: Run Task → gdbserver runs everything. You'll see the compile, the copy, then Listening on port 2345. At that point the board is waiting with your program paused, ready for a debugger to connect.

Here's the complete .vscode/tasks.json from #4, #5, and #6 together:

{
  // Deploy + on-board debug pipeline for the DE10-Nano.
  // All board/host values come from .vscode/settings.json (${config:xxx}).
  // launch.json's preLaunchTask is "gdbserver", which chains: build -> deploy -> gdbserver.
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build",
      "type": "shell",
      "command": "C:/msys64/mingw64/bin/mingw32-make.exe",
      "group": { "kind": "build", "isDefault": true },
      "problemMatcher": ["$gcc"]
    },
    {
      "label": "deploy",
      "type": "shell",
      "command": "scp",
      "args": [
        "-o", "StrictHostKeyChecking=accept-new",
        "${config:bin}",
        "${config:user}@${config:host}:${config:remoteDir}/${config:bin}"
      ],
      "dependsOn": ["build"],
      "problemMatcher": []
    },
    {
      "label": "gdbserver",
      "type": "shell",
      "command": "ssh",
      "args": [
        "-o", "StrictHostKeyChecking=accept-new",
        "${config:user}@${config:host}",
        "chmod +x ${config:remoteDir}/${config:bin} && exec gdbserver --once :${config:gdbPort} ${config:remoteDir}/${config:bin}"
      ],
      "isBackground": true,
      "dependsOn": ["deploy"],
      "problemMatcher": {
        "owner": "gdbserver",
        "pattern": {
          "regexp": "^(__gdbserver_never_matches__)$",
          "file": 1,
          "location": 1,
          "message": 1
        },
        "background": {
          "activeOnStart": true,
          "beginsPattern": ".",
          "endsPattern": "Listening on port"
        }
      }
    }
  ]
}

Final Verdict

This is where the setup really started saving me time. One command now builds, copies, and gets the board ready to debug, and it stops at the first failure, so I can't debug an old binary by mistake anymore. The never-matching pattern looks strange, but once you see it's only there to satisfy VS Code, the background matcher is simple: wait for Listening on port, then carry on.

If you're doing embedded Linux from Windows, I'd set up this chain before writing any real application code. The next step is launch.json, which ties it to F5, so one keypress takes you from editing code to stepping through it on the board.