Skip to content

Releases

v0.11.4 - Download Ghidra is 7.7x faster (70s -> 9s)

Choose a tag to compare

@spix18 spix18 released this 09 Oct 00:35

"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

Choose a tag to compare

@spix18 spix18 released this 08 Oct 09:47

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 githubToken config field (the 13th), falling back to GH_TOKEN, GITHUB_TOKEN, then
    gh auth token. On a host where gh is 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 the x-ratelimit-* headers
    (which say when it resets) — which is why all the panel could show was GitHub 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 Ghidra could 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 keeps Expand-Archive on Windows and runs unzip -q -o <zip> -d <dest>
    elsewhere — unzip rather than python -m zipfile because it restores the archive's executable
    bits, which analyzeHeadless needs.

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

Choose a tag to compare

@spix18 spix18 released this 08 Oct 08:14

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
    inline min-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 with aria-describedby.

Correctness

  • The download percentage could never render: the branch is gated on
    install.total, but the /status poll copied only running/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

Choose a tag to compare

@spix18 spix18 released this 08 Oct 00:06

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

Choose a tag to compare

@spix18 spix18 released this 07 Oct 23:07

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 changing pythonVer from 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-dir built
    its roots by scanning A:–Z:, which yields nothing on Linux or macOS.
  • MCP_OUT was missing a field it declared. The schema sets
    additionalProperties: false and the tool returns mode, 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.exe imported and analysed (1133 functions), the
    PyGhidra bridge started, the REST server bound in unified mode, /health reporting
    program_loaded: true, then a clean stop and shutdown — confirming Windows did not regress.

Install: dsh plugin add dsh-ghidra