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/mainmakes the file executable. Windows files don't have Linux's "executable" permission, so the file usually arrives fromscpwithout it. Skip this and you getPermission denied. -
exec gdbserver --once :2345 /home/root/main:-
gdbserveris a small debugging helper that runs on the board. It starts your program, pauses it before the first line ofmain, 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 getgdbserver: not found, check withwhich gdbserveron the board. -
:2345is the port, fromgdbPort. The empty part before the colon means "accept connections on any of the board's network addresses". -
--oncemakes gdbserver exit after the first debug session instead of waiting for another, so each run starts clean and no old gdbserver holds the port. -
execreplaces 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:
When VS Code seesProcess /home/root/main created; pid = 812 Listening on port 2345Listening on port, the task is marked ready, and anything waiting on it (like the debugger'spreLaunchTask) 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.