Skip to content

Commit d70c77d

Browse files
committed
feat: SourcePawn remote debugger for VS Code
Debug SourcePawn plugins running on a live SourceMod server from VS Code, over DAP: breakpoints (conditional, hit-count, logpoints and non-freezing snapshot logpoints), call stack, variable inspection and editing, stepping, Debug Console REPL, data breakpoints, memory view, and a function profiler. Layout: dap/ DAP/TCP/profiler crate (ours) sp-console-debugger/ vendored upstream, NOT committed -- reconstructed at build time from patches/*.commit + *.patch vscode/ the VS Code extension and its test suites docs/ usage, architecture, protocol, troubleshooting Security posture: the debug port listens on 127.0.0.1 by default, since a session can read and write plugin memory and can hold the game thread on a breakpoint. Exposing it beyond loopback requires a shared token, which is demanded before any request is served; without one the listener refuses to start. Lifecycle: the accept thread and all client threads are owned and joined on teardown, so unloading or reloading the extension -- and restarting the server with an editor attached -- neither freezes nor crashes the host process. Tests: unit suites plus two integration suites driven against a mock srcds (full DAP session, and extension load/unload/reload/shutdown), alongside the upstream console regression harness.
1 parent c01ca99 commit d70c77d

93 files changed

Lines changed: 29294 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/build.yml‎

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
name: build
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- "v*" # e.g. v1.0.4 -> builds everything and cuts a release
8+
schedule:
9+
- cron: "30 03 01 */3 *" # Artifacts expire every 3 months
10+
11+
permissions:
12+
contents: write # needed by the release job to create/upload a GitHub release
13+
14+
jobs:
15+
build:
16+
name: build (${{ matrix.os }} / ${{ matrix.sm.label }})
17+
runs-on: ${{ matrix.os }}
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
os:
22+
- ubuntu-22.04
23+
- windows-latest
24+
# Build against BOTH SourceMod lines. The extension API version is baked
25+
# in from the SDK headers at compile time: master (1.13-dev) is ext API 9,
26+
# which stable 1.12 servers refuse with "Extension version is too new to
27+
# load (9, max is 8)". Shipping one package per line lets server owners
28+
# pick the one matching their SourceMod install.
29+
# (Bump branch/label here when a new SourceMod stable rolls.)
30+
sm:
31+
- { branch: "1.12-dev", label: "sm1.12" } # stable (ext API 8)
32+
- { branch: "master", label: "sm1.13" } # dev (ext API 9)
33+
34+
steps:
35+
- name: Prepare env
36+
shell: bash
37+
run: |
38+
echo "GITHUB_SHA_SHORT=${GITHUB_SHA::7}" >> $GITHUB_ENV
39+
40+
# Pin one interpreter for the whole job. setup-python registers it on
41+
# GITHUB_PATH, so `python` resolves to the SAME environment in every step
42+
# regardless of shell -- critical on Windows, where the default steps use
43+
# PowerShell but "Build SourcePawn VM" uses Git Bash, and the two would
44+
# otherwise pick different pythons (only one of which has our pip installs).
45+
- name: Set up Python
46+
uses: actions/setup-python@v5
47+
with:
48+
python-version: "3.11"
49+
50+
- name: Install (Linux)
51+
if: runner.os == 'Linux'
52+
run: |
53+
sudo dpkg --add-architecture i386
54+
sudo apt-get update
55+
sudo apt-get install -y --no-install-recommends \
56+
gcc-multilib g++-multilib libstdc++6 lib32stdc++6 \
57+
libc6-dev libc6-dev-i386 linux-libc-dev \
58+
linux-libc-dev:i386 clang
59+
echo "CC=clang" >> $GITHUB_ENV
60+
echo "CXX=clang++" >> $GITHUB_ENV
61+
62+
- name: Getting SourceMod (${{ matrix.sm.branch }})
63+
uses: actions/checkout@v4
64+
with:
65+
repository: alliedmodders/sourcemod
66+
ref: ${{ matrix.sm.branch }}
67+
path: sourcemod
68+
submodules: recursive
69+
70+
- name: Switch to debug symbol sourcepawn branch
71+
run: |
72+
cd sourcemod/sourcepawn
73+
git remote add peace https://github.com/peace-maker/sourcepawn.git
74+
git fetch peace
75+
git switch debug_api_symbols
76+
git submodule update --init --recursive
77+
78+
- name: Getting ambuild
79+
run: |
80+
# SourceMod's sourcepawn/configure.py does `from pkg_resources import
81+
# parse_version`. pkg_resources lives in setuptools, but setuptools 81+
82+
# REMOVED it (verified: 82.0.1 no longer has the module), so we must
83+
# pin below 81 -- a plain `--upgrade setuptools` would sail past the
84+
# last working version and break configure.py on every OS. Thanks to
85+
# the setup-python step above, this lands in the one interpreter every
86+
# step (PowerShell and Git Bash alike) resolves to.
87+
python -m pip install --upgrade pip wheel
88+
python -m pip install "setuptools<81"
89+
pip install git+https://github.com/alliedmodders/ambuild
90+
91+
- name: Getting own repository
92+
uses: actions/checkout@v4
93+
with:
94+
path: sp-debugger
95+
96+
# The vendored sp-console-debugger/ tree is NOT committed -- reconstruct it
97+
# from the pinned upstream commit plus our patch (docs/updating-upstream.md).
98+
- name: Fetch vendored sp-console-debugger (upstream + patch)
99+
working-directory: sp-debugger
100+
shell: bash
101+
run: |
102+
set -euo pipefail
103+
sha="$(cat patches/sp-console-debugger.commit)"
104+
test -n "$sha"
105+
git init sp-console-debugger
106+
git -C sp-console-debugger fetch --depth 1 https://github.com/peace-maker/sp-console-debugger "$sha"
107+
git -C sp-console-debugger reset --hard FETCH_HEAD
108+
git -C sp-console-debugger apply --whitespace=nowarn ../patches/sp-console-debugger.patch
109+
110+
- name: Compiling Extension
111+
working-directory: sp-debugger
112+
run: |
113+
python configure.py --enable-optimize --targets x86,x86_64 --sm-path="${{ github.workspace }}/sourcemod"
114+
ambuild objdir
115+
116+
- name: Build SourcePawn VM
117+
working-directory: sourcemod/sourcepawn
118+
shell: bash
119+
run: |
120+
python configure.py --enable-optimize --targets x86,x86_64
121+
ambuild objdir
122+
echo "Built libsourcepawn artifacts:"
123+
find objdir -name 'libsourcepawn.*' -print
124+
125+
# The patched (debug_api_symbols) VM MUST ship with the extension -- the
126+
# profiler and symbol-aware debugging depend on it. Locate the built libs
127+
# by glob (ambuild's exact subfolder can change) and copy to the names the
128+
# extension loads: bin/sourcepawn.jit.x86.{so,dll} and bin/x64/sourcepawn.vm.{so,dll}.
129+
# Fail loudly if a lib is missing so we never ship a package without the VM.
130+
- name: Package SourcePawn VM (Linux)
131+
if: runner.os == 'Linux'
132+
shell: bash
133+
working-directory: sourcemod/sourcepawn
134+
run: |
135+
set -euo pipefail
136+
DEST="${{ github.workspace }}/sp-debugger/objdir/package/addons/sourcemod/bin"
137+
mkdir -p "$DEST/x64"
138+
x86_lib="$(find objdir -path '*-x86/libsourcepawn.so' ! -path '*x86_64*' | head -n1)"
139+
x64_lib="$(find objdir -path '*x86_64/libsourcepawn.so' | head -n1)"
140+
echo "x86: ${x86_lib:-<none>}"
141+
echo "x64: ${x64_lib:-<none>}"
142+
[ -n "$x86_lib" ] || { echo "::error::libsourcepawn.so (x86) not found"; exit 1; }
143+
[ -n "$x64_lib" ] || { echo "::error::libsourcepawn.so (x86_64) not found"; exit 1; }
144+
cp "$x86_lib" "$DEST/sourcepawn.jit.x86.so"
145+
cp "$x64_lib" "$DEST/x64/sourcepawn.vm.so"
146+
ls -l "$DEST/sourcepawn.jit.x86.so" "$DEST/x64/sourcepawn.vm.so"
147+
148+
- name: Package SourcePawn VM (Windows)
149+
if: runner.os == 'Windows'
150+
shell: bash
151+
working-directory: sourcemod/sourcepawn
152+
run: |
153+
set -euo pipefail
154+
DEST="${{ github.workspace }}/sp-debugger/objdir/package/addons/sourcemod/bin"
155+
mkdir -p "$DEST/x64"
156+
x86_lib="$(find objdir -path '*-x86/libsourcepawn.dll' ! -path '*x86_64*' | head -n1)"
157+
x64_lib="$(find objdir -path '*x86_64/libsourcepawn.dll' | head -n1)"
158+
echo "x86: ${x86_lib:-<none>}"
159+
echo "x64: ${x64_lib:-<none>}"
160+
[ -n "$x86_lib" ] || { echo "::error::libsourcepawn.dll (x86) not found"; exit 1; }
161+
[ -n "$x64_lib" ] || { echo "::error::libsourcepawn.dll (x86_64) not found"; exit 1; }
162+
cp "$x86_lib" "$DEST/sourcepawn.jit.x86.dll"
163+
cp "$x64_lib" "$DEST/x64/sourcepawn.vm.dll"
164+
ls -l "$DEST/sourcepawn.jit.x86.dll" "$DEST/x64/sourcepawn.vm.dll"
165+
166+
# Confirm both extension binaries shipped. Fail loudly otherwise so a
167+
# packaging regression can't slip an empty build out. (The runtime config
168+
# is no longer packaged -- the extension generates it on first load.)
169+
- name: Verify package contents
170+
shell: bash
171+
working-directory: sp-debugger
172+
run: |
173+
set -euo pipefail
174+
for f in objdir/package/addons/sourcemod/extensions/sp-debugger.ext.* \
175+
objdir/package/addons/sourcemod/extensions/x64/sp-debugger.ext.*; do
176+
[ -f "$f" ] || { echo "::error::$f missing from package"; exit 1; }
177+
echo "ok: $f"
178+
done
179+
180+
# The ONLY artifact per build: the package directory. Release assets are
181+
# assembled from these by the release job -- no duplicate "release-server"
182+
# artifacts cluttering the run.
183+
- name: Uploading package
184+
uses: actions/upload-artifact@v4
185+
with:
186+
name: sp-debugger-${{ matrix.os }}-${{ matrix.sm.label }}-${{ env.GITHUB_SHA_SHORT }}
187+
path: sp-debugger/objdir/package
188+
189+
vsix:
190+
name: build (vscode .vsix)
191+
runs-on: ubuntu-22.04
192+
steps:
193+
- name: Getting own repository
194+
uses: actions/checkout@v4
195+
196+
- name: Setup Bun
197+
uses: oven-sh/setup-bun@v2
198+
with:
199+
bun-version: latest
200+
201+
- name: Install dependencies
202+
working-directory: vscode
203+
run: bun install --frozen-lockfile
204+
205+
# On a tag push, stamp the extension version from the tag (v1.2.3 -> 1.2.3)
206+
# so the .vsix filename and manifest match the release.
207+
- name: Sync version from tag
208+
if: startsWith(github.ref, 'refs/tags/v')
209+
working-directory: vscode
210+
run: |
211+
VERSION="${GITHUB_REF_NAME#v}"
212+
echo "Setting extension version to $VERSION"
213+
npm version "$VERSION" --no-git-tag-version --allow-same-version
214+
215+
# The extension is compiled with tsc (not bundled), so its single runtime
216+
# dependency (@vscode/debugadapter) must ship inside the .vsix -- do NOT
217+
# pass --no-dependencies here. vsce runs the vscode:prepublish hook
218+
# (bun run compile) automatically before packaging.
219+
- name: Package .vsix
220+
working-directory: vscode
221+
run: bunx vsce package -o sp-debugger.vsix
222+
223+
- name: Upload .vsix artifact
224+
uses: actions/upload-artifact@v4
225+
with:
226+
name: sp-debugger-vsix
227+
path: vscode/sp-debugger.vsix
228+
229+
release:
230+
name: release
231+
needs: [build, vsix]
232+
if: startsWith(github.ref, 'refs/tags/v')
233+
runs-on: ubuntu-22.04
234+
steps:
235+
- name: Download all artifacts
236+
uses: actions/download-artifact@v4
237+
with:
238+
path: artifacts
239+
240+
# Each sp-debugger-<os>-<sm>-<sha> artifact downloads as the package
241+
# directory; archive each one into a release asset named by platform and
242+
# SourceMod line (so server owners grab the one matching their install).
243+
- name: Collect release assets
244+
run: |
245+
set -euo pipefail
246+
mkdir -p release
247+
cp -v artifacts/sp-debugger-vsix/*.vsix release/
248+
for dir in artifacts/sp-debugger-*/; do
249+
name="$(basename "$dir")"
250+
[ "$name" = "sp-debugger-vsix" ] && continue
251+
label="$(echo "$name" | grep -oE 'sm[0-9]+\.[0-9]+')"
252+
case "$name" in
253+
*ubuntu*) tar -czf "release/sp-debugger-server-linux-${label}.tar.gz" -C "$dir" . ;;
254+
*windows*) (cd "$dir" && zip -qr "${GITHUB_WORKSPACE}/release/sp-debugger-server-windows-${label}.zip" .) ;;
255+
*) echo "::warning::unrecognized artifact $name (skipped)" ;;
256+
esac
257+
done
258+
echo "Release assets:"
259+
ls -l release
260+
261+
- name: Create GitHub release
262+
uses: softprops/action-gh-release@v2
263+
with:
264+
files: release/*
265+
generate_release_notes: true
266+
fail_on_unmatched_files: true

‎.github/workflows/test.yml‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: test
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
jobs:
8+
# Fast, dependency-free: the VS Code extension's unit test suite. The
9+
# integration test self-skips here (no mock srcds staged in this job).
10+
unit:
11+
name: unit tests (bun)
12+
runs-on: ubuntu-22.04
13+
timeout-minutes: 10
14+
steps:
15+
- name: Getting own repository
16+
uses: actions/checkout@v5
17+
18+
- name: Setup Bun
19+
uses: oven-sh/setup-bun@v2
20+
with:
21+
bun-version: latest
22+
23+
- name: Install dependencies
24+
working-directory: vscode
25+
run: bun install --frozen-lockfile
26+
27+
- name: Run unit tests
28+
working-directory: vscode
29+
run: bun test
30+
31+
build:
32+
name: regression test
33+
runs-on: ubuntu-22.04
34+
timeout-minutes: 60
35+
steps:
36+
- name: Install dependencies
37+
run: |
38+
sudo dpkg --add-architecture i386
39+
sudo apt-get update
40+
sudo apt-get install -y --no-install-recommends \
41+
gcc-multilib g++-multilib libstdc++6 lib32stdc++6 \
42+
libc6-dev libc6-dev-i386 linux-libc-dev \
43+
linux-libc-dev:i386 lib32z1-dev clang
44+
echo "CC=clang" >> $GITHUB_ENV
45+
echo "CXX=clang++" >> $GITHUB_ENV
46+
47+
- name: Getting own repository
48+
uses: actions/checkout@v5
49+
50+
# The vendored sp-console-debugger/ tree is NOT committed -- reconstruct it
51+
# from the pinned upstream commit plus our patch (docs/updating-upstream.md).
52+
- name: Fetch vendored sp-console-debugger (upstream + patch)
53+
run: |
54+
set -euo pipefail
55+
sha="$(cat patches/sp-console-debugger.commit)"
56+
test -n "$sha"
57+
git init sp-console-debugger
58+
git -C sp-console-debugger fetch --depth 1 https://github.com/peace-maker/sp-console-debugger "$sha"
59+
git -C sp-console-debugger reset --hard FETCH_HEAD
60+
git -C sp-console-debugger apply --whitespace=nowarn ../patches/sp-console-debugger.patch
61+
62+
# Restore the mock srcds environment that setup.sh assembles (it lives
63+
# inside the reconstructed tree, so this must run after the fetch above).
64+
# The key covers setup.sh, which carries the upstream commit pins -- moving
65+
# a pin therefore builds a fresh environment instead of reusing a stale one.
66+
# Restore/save are split on purpose: a run that fails halfway must not
67+
# publish a half-built environment for every later run to inherit.
68+
- name: Restore test environment
69+
id: mock-cache
70+
uses: actions/cache/restore@v6
71+
with:
72+
path: sp-console-debugger/tests/mock
73+
key: ${{ runner.os }}-tests-mock-${{ hashFiles('sp-console-debugger/tests/setup.sh') }}
74+
75+
- name: Setup test environment
76+
working-directory: sp-console-debugger/tests
77+
run: ./setup.sh
78+
79+
- name: Run console regression tests
80+
working-directory: sp-console-debugger/tests
81+
run: ./run_tests.sh
82+
83+
# DAP/TCP regression: drives the extension's debug adapter (the dap/ crate)
84+
# like the VS Code client would, reusing the mock env built above. The
85+
# runner stages the gamedir and then executes the integration test suite
86+
# (vscode/src/__tests__/integration/) via bun test.
87+
- name: Setup Bun
88+
uses: oven-sh/setup-bun@v2
89+
with:
90+
bun-version: latest
91+
92+
- name: Install extension dependencies
93+
working-directory: vscode
94+
run: bun install --frozen-lockfile
95+
96+
- name: Run DAP regression test
97+
run: vscode/tests/dap/run_dap_tests.sh
98+
99+
- name: Save test environment
100+
if: success() && steps.mock-cache.outputs.cache-hit != 'true'
101+
uses: actions/cache/save@v6
102+
with:
103+
path: sp-console-debugger/tests/mock
104+
key: ${{ runner.os }}-tests-mock-${{ hashFiles('sp-console-debugger/tests/setup.sh') }}

‎.gitignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
objdir
2+
vscode/node_modules
3+
vscode/out
4+
vscode/*.vsix
5+
.venv
6+
.claude
7+
.ruvector
8+
bkp
9+
sp-console-debugger

0 commit comments

Comments
 (0)