Repository navigation
Releases
v0.11.4 - Download Ghidra is 7.7x faster (70s -> 9s)
"The download speed is slow" — that was our bug, not your line
Reported after 0.11.3 made Download Ghidra work: the 543 MB Ghidra zip took about
70 seconds on a 600 Mbps connection.
Root cause
On one host, against one signed URL:
| method | 50 MB | rate |
|---|---|---|
curl --http1.1 (baseline) |
1.42 s | 35.2 MB/s |
fetch + arrayBuffer() |
7.57 s | 6.6 MB/s |
fetch + Readable.fromWeb + pipeline (the plugin before this release) |
8.62 s | 5.8 MB/s |
same, with the web stream's highWaterMark raised to 1 MB |
8.73 s | 5.7 MB/s |
fetch + for await (… of res.body) |
9.17 s | 5.5 MB/s |
node:https data events |
1.59 s | 31.4 MB/s |
Every undici consumption style is capped identically — even arrayBuffer(), which applies no
backpressure at all — and raising highWaterMark changes nothing. The bottleneck is undici's own
read path, not our Transform, not createWriteStream, not the disk (a no-disk, network-only
control was equally slow at 7.8 MB/s). No proxy was set, the CDN resolves to A records only, and
--dns-result-order=ipv4first / ipv6first made no difference. curl itself fluctuates between
19 and 50 MB/s on this host, so GitHub imposes no hard cap — only Node's fetch is a flat line.
Fix
downloadToFile() now streams over node:https, follows up to five redirects by hand (GitHub
sends browser_download_url on to a signed release-assets.githubusercontent.com URL), rejects a
non-200 with download failed: HTTP <code>, and reports every chunk through a callback so the
progress bar keeps working. The small GitHub API JSON still uses fetch on purpose — the 403
quota reporting from 0.11.3 lives on that path.
Measured on the real asset
before (fetch) : 543.3 MB in 70.48 s -> 7.7 MB/s
after (node:https) : 543.3 MB in 9.14 s -> 59.4 MB/s
on disk 569649598 bytes = the release's asset.size -> byte-identical
7.7x faster, same bytes.
The regression test had to be rewritten, not relaxed
probe-github-install.mjs fed the download through a globalThis.fetch stub, so this change
turned it red — which is itself the proof the fix took effect. The download layer is now tested
against a local node:http server (302 followed, byte-identity, streaming callback, 404 error,
redirect-loop cap) plus an assertion that the download still completes when globalThis.fetch
is replaced by a throwing stub. A .zip.sha256 decoy in the release fixture keeps the
asset-selection regex honest. 20 → 26 checks.
Full suite green: verify-load, probe-v4-routes 16/16, probe-availability,
probe-client-apply 34/34, probe-contrast 27/27, probe-skill-registry 10/10,
probe-github-install 26/26.
v0.11.3 — Download Ghidra: fix the GitHub 403 and the PowerShell-only extraction
Fix for the reported Install failed: GitHub API 403 on the Download Ghidra button.
Root cause
The release lookup called api.github.com anonymously, and anonymous requests are capped at
60/hour per IP. Reproduced verbatim with curl, using the same two headers the plugin sends:
HTTP/1.1 403 rate limit exceeded
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 X-RateLimit-Used: 60
{"message":"API rate limit exceeded for 105.98.99.57. ..."}
With every header removed the response is the identical 403, so the plugin's headers were never
the issue. With Authorization: Bearer the same endpoint returns 200 and a 5000/hour limit.
What changed
- New
githubTokenconfig field (the 13th), falling back toGH_TOKEN,GITHUB_TOKEN, then
gh auth token. On a host whereghis already logged in, the button works with no
configuration at all. - The error now says what actually happened. The old throw kept only the status text and
discarded the response body (which names the IP and the remedy) and thex-ratelimit-*headers
(which say when it resets) — which is why all the panel could show wasGitHub API 403. It now
reports that the quota is exhausted rather than denied, when it resets, the server's own words,
and the three ways to raise the limit. - The asset download deliberately sends no
Authorization— it is a public asset, and a
cross-host redirect would drop the header anyway. Download Ghidracould never work on Linux or macOS at all. Extraction shelled out to
powershell -Command Expand-Archive, so on those platforms the button could only fail with
ENOENT. It now keepsExpand-Archiveon Windows and runsunzip -q -o <zip> -d <dest>
elsewhere —unziprather thanpython -m zipfilebecause it restores the archive's executable
bits, whichanalyzeHeadlessneeds.
Verification
New probe-github-install.mjs (20/20) drives the real route with a stubbed fetch and guards the
token fallback chain, the error text, the extraction plan, the Bearer header on the API call and
its absence on the download, plus an equality guard between the panel's hand-maintained field
whitelist and the Config schema. The full suite is green, and the previously failing operation was
re-run against the real API: 200 in 523ms, ratelimit 4997/5000.
Full changelog: PATCHES.md — section 0.11.3.
v0.11.2 — accessibility and theming hardening, plus a leaner tree
Two audit rounds over the settings panel, and the fixes they turned up.
Accessibility
- The
@media (pointer: coarse)44px rule never applied: every control carries an
inlinemin-height, and an inline declaration outranks any non-!important
author rule. It now does apply. - Live regions were inserted together with their text, so screen readers often
missed the announcement (WCAG 4.1.3). Install, doctor, save and browse regions
are now permanent containers whose text changes. - Field help was reachable only through
title(mouse-only). It is now a real
paragraph wired witharia-describedby.
Correctness
- The download percentage could never render: the branch is gated on
install.total, but the/statuspoll copied onlyrunning/phase/pct/error/home.
Theming
- 12 hardcoded font sizes and 3 line-heights bypassed the host type scale; two
were 10px, below the host floor of 11px. All now use--dsw-font-*tokens. - Control borders moved to
--dsw-alias-border-l4, the token the host's own
controls use (0.5px is host convention). Control-boundary contrast rises
1.32 -> 1.45:1 light and 1.68 -> 1.91:1 dark.
Lean-ness
- 19 dead development scripts removed (-9,253 lines), the contrast probe's alpha
truncation fixed, and several never-called actions and inert rules deleted.
Verification
verify-load passes, and the probes report 34/34 (client contract), 27/27
(contrast, both themes), 16/16 (routes), 10/10 (skill registry) and the
availability probe pass.
Published as dsh-ghidra@0.11.2 on npm.
Full diff: v0.11.1...v0.11.2
dsh-ghidra 0.11.1 - remove personal names and host paths
0.11.1 - remove personal names and host paths
The files shipped in the npm tarball (README.md, PATCHES.md, LINUX-PORT.md,
sync-installed.mjs and lib/) carried two kinds of machine/identity detail:
- the Linux port host's home directory, written out in full in
LINUX-PORT.md
(the venv path, the XDG extension directory the JVM printed, the measured
pythonCommand()result, and the work directory in the sync section); - the Windows account directory, in five files.
They are neutral now - ~ for a home directory and <user> for the account
name - and LINUX-PORT.md section 3.1 says so explicitly, so the quoted JVM dump
stays honest about what was elided.
No behaviour changed. The repository's local git identity, and the author of the
three commits already on main, were rewritten to the project owner; commit
contents are untouched.
If you installed 0.11.0, upgrade to 0.11.1.
dsh-ghidra 0.11.0 - Linux and macOS support, plus ghidra_doctor
dsh-ghidra 0.11.0 — runs on Linux and macOS, and can now tell you why it does not
Upstream 0.10.1 was Windows-only. This release makes the plugin work on Linux and macOS,
adds a built-in environment self-check, and keeps Windows behaviour unchanged.
Cross-platform
Ghidra detection no longer looks only for support/analyzeHeadless.bat. It accepts the
extensionless launcher, searches the paths the three platforms actually use, and reports
where it found Ghidra:
- Linux —
/opt,/usr/local,/usr/share,$HOME(XDG user dir) - macOS — Homebrew's
Caskroom/<version>/…,Ghidra.app/Contents/Resources/ghidra,/Applications,~/Applications - Windows — unchanged, plus
%USERPROFILE%
The interpreter is resolved instead of assumed. Upstream hardcoded py -<ver>, which
does not exist off Windows. The plugin now honours DSH_GHIDRA_PYTHON, then probes
python<ver>, python3, python, then $VIRTUAL_ENV, ~/.venv and ~/pyghidra-venv —
and checks that the candidate can actually import pyghidra before using it.
Process cleanup works off Windows too. killProcessesUsing() used wmic/netstat and
returned 0 there, so stale JVMs held the port; it now walks the tree with pgrep -P and
escalates SIGTERM → SIGKILL. findPidByPort() uses ss -ltnp, falling back to lsof.
The Ghidra user directory was wrong on both non-Windows platforms
This one fails silently — Ghidra starts normally with the extension simply absent from
the classpath — so it is worth stating precisely. The base directory now mirrors
upstream-build.gradle's own resolveGhidraUserDir:
| Platform | Base | Fallback |
|---|---|---|
| macOS | ~/Library/ghidra |
— |
| Windows | %APPDATA%\ghidra |
~/AppData/Roaming/ghidra |
| other | $XDG_CONFIG_HOME/ghidra |
~/.config/ghidra |
The versioned leaf is not invented either: an existing ghidra_<ver>* directory wins,
because the on-disk suffix is not always guessable (a real install reported
ghidra_12.1.2_DEV, the _DEV coming from application.release.name).
ghidra_doctor
One read-only call that reports the platform, the Ghidra home and how it was found, the
headless launcher, the version, whether the install path contains non-ASCII, the PyGhidra
launcher, the interpreter and whether pyghidra imports, Java, the resolved user settings
directory, the GhidraMCP jar, the data root, project-directory writability, and whether
anything already holds the MCP port.
Reachable three ways that share a single collector — the ghidra_doctor tool, the
/api/dsh-ghidra/doctor route, and the panel's Run doctor button — because two
hand-maintained check lists eventually disagree, and "the panel says OK while the tool says
FAIL" is the one failure a self-check must not have. It is always advertised: gating a
diagnostic behind the bridge it diagnoses would defeat it. The panel now prints each failing
check's remediation rather than hiding it in a tooltip.
Also fixed
- The interpreter cache ignored the config.
pythonCommand()memoised into a single
unconditional slot, so changingpythonVerfrom 3.13 to 3.12 kept the old interpreter
until DSH restarted. The cache is now keyed by the requested version. - The folder picker had no starting point off Windows.
/api/dsh-ghidra/list-dirbuilt
its roots by scanningA:–Z:, which yields nothing on Linux or macOS. MCP_OUTwas missing a field it declared. The schema sets
additionalProperties: falseand the tool returnsmode, so the framework rejected the
server's own output with"value.mode" is not a declared property.- The shipped dev scripts hardcoded one Windows profile path; they now resolve through
lib/dev-env.mjs.
Verification
verify-load.mjs— all checks PASS: 219 definitions = 6 always-on + 45 bridge-gated + 168 REST.probe-v4-routes.mjs— 16/16, including the doctor through the real HTTP handler.probe-availability.mjs— gating open/close/idempotence, 3 rounds with 0 duplicates.probe-client-apply.mjs— 26/26.- Full openFlow end to end on Windows:
cmd.exeimported and analysed (1133 functions), the
PyGhidra bridge started, the REST server bound in unified mode,/healthreporting
program_loaded: true, then a clean stop and shutdown — confirming Windows did not regress.
Install: dsh plugin add dsh-ghidra