feat(heap-dump): add --redact, --compress, and --open flags - #56
Open
parttimenerd wants to merge 40 commits into
Open
parttimenerd wants to merge 40 commits into
parttimenerd wants to merge 40 commits into
Conversation
- Embeds hprof-redact binaries (linux/amd64, linux/arm64, darwin/arm64, windows/amd64) via //go:embed; extracted to ~/.cache/cf-java-plugin/ on first use (SHA8-keyed, reused on subsequent runs). - --compress: passes gz=1 to jmap (JDK 17+) to compress the dump on the remote container before transfer, saving bandwidth. jvmmon path falls back to post-creation gzip on the container. Output is .hprof.gz. - --redact: pipes the downloaded dump through hprof-redact (lean mode: zeros primitive arrays only). Output is -redacted.hprof or -redacted.hprof.gz when --compress is also set. - --redact-complete: like --redact but zeros all primitive values (instance scalar fields + CLASS_DUMP statics) via two-pass redaction. - --compress and --redact can be combined: compress on remote to reduce transfer, redact locally (hprof-redact reads .hprof.gz natively). - Adds FindHeapDumpGzFile to utils for locating *.hprof.gz on the container (jvmmon+compress path). - Extracts osWindows and cmdHeapDump constants to satisfy goconst.
- hprof-analyzer release.yml: add aarch64-pc-windows-msvc target on windows-11-arm runner; compile-all includes GOOS=windows GOARCH=arm64 - redact.go: embed dist/hprof-redact-windows-arm64.exe (stub until v0.3.1 ships); add windows/arm64 case; guard against 0-byte stubs - cfutils.go: add CopyOverCatGunzip (io.Pipe + compress/gzip for transparent streaming decompression) and ProbeRemoteFileGzip (checks gzip magic bytes 1f8b via SSH) - cf_cli_java_plugin.go: heap-dump SSHCommand now uses shell-level gz probe (jmap -h | grep gz) so jmap auto-uses gz=1 on JDK 17+; Go post-command probes the remote file magic bytes to decide: - remote gz + no --compress → CopyOverCatGunzip, save as .hprof - remote gz + --compress → CopyOverCat, save as .hprof.gz - not gz + --compress → warn JDK 17+ required, save uncompressed Removes @JMAP_GZ Go expansion (replaced by shell probe); keeps @COMPRESS_FLAG for jvmmon path
- appInstanceIndexSet: simonleung8/flags IsSet() returns true for any registered flag whose non-zero default was set at init time, making it impossible to distinguish user-provided from default. Compare against known default (-1) instead. - CheckRequiredTools wrapped in !options.DryRun guard so dry-run works without CF login or SSH access. - var err declaration hoisted before GenerateFiles block (needed after CheckRequiredTools was scoped inside the if block).
…-dump - Feature list in intro - Examples: --redact, --redact-complete, --compress, combined usage - New "Heap Dump Privacy" subsection: redaction modes table, what is preserved, file naming, supported platforms - New "Compressed Transfer" subsection: JDK 17+ requirement, fallback behaviour, transparent gz transfer without --compress - CHANGELOG [Unreleased]: all four new behaviours
- Use context.WithTimeout(context.Background(), 5s) for srv.Shutdown instead of the request context (which may already be canceled); suppress contextcheck lint with explanation since this is intentional - Add empty title arg to Windows `cmd /c start` to avoid URL being interpreted as the window title - Replace non-blocking done-channel check in test with a 2s timeout select to avoid a false-pass race condition
…eakage and restrict access to single file
… in serveFileOnce
…s only affects local file format
golangci-lint typecheck fails when the dist/ hprof-redact binaries are missing (go:embed pattern not satisfied). Add a download step mirroring build.py, placed after the existing jstall download and before lint.
The previous attempt tried to curl the binaries directly, but they are packed inside tar.gz/zip archives. Add --deps-only flag to build.py to run only the download+extraction step (no go build), and use it in the PR validation workflow before golangci-lint.
dbriemann
reviewed
Sep 18, 2026
ansteiner
requested changes
Sep 18, 2026
- redact: delete partial output file on failure instead of leaving it behind; add --redact-keep-on-error flag to preserve it when needed - open: use sync.Once to guard close(doneCh)+srv.Shutdown so concurrent GETs cannot panic via double-close - open: print error and manual-open URL when browser launch fails
serveFileOnce: - add timeout parameter (10 min in production) — CLI no longer hangs forever if the browser never fetches the dump - add WriteTimeout (30s) to prevent stalled clients holding a goroutine - print stderr message when timeout expires so user knows why CLI exited tests: - TestServeFileOnce_TimeoutClosesServer: done closes, server stops after timeout - TestServeFileOnce_ConcurrentGETsNoPanic: regression for sync.Once fix - TestPipeHeapDumpThroughRedact_ErrorDeletesPartial: partial file removed on failure - TestPipeHeapDumpThroughRedact_KeepOnError: partial file kept with keepOnError=true - TestPipeHeapDumpThroughRedact_HappyPath: success deletes source, returns output path
ansteiner
previously approved these changes
Sep 18, 2026
dbriemann
reviewed
Sep 21, 2026
dbriemann
reviewed
Sep 21, 2026
Comment on lines
+275
to
+283
| wait := func() error { | ||
| return cat.Wait() | ||
| } | ||
|
|
||
| go func() { | ||
| _ = pw.CloseWithError(wait()) | ||
| }() | ||
|
|
||
| return pr, wait, nil |
Contributor
There was a problem hiding this comment.
Wait() will be called twice, which will produce an error. Only one goroutine should call cat.Wait().
Member
Author
There was a problem hiding this comment.
Fixed: used sync.Once to cache the cat.Wait() result, making the closure idempotent. The goroutine and the caller both see the correct exit error regardless of which runs first.
The goroutine that closes pw already calls wait() (which calls cat.Wait()). The caller in cf_cli_java_plugin.go then calls waitRemote() — the same closure — a second time. The second cat.Wait() returns an error because the process was already reaped. Fix: cache the result with sync.Once so wait() is idempotent. Both the goroutine and the caller see the correct exit error regardless of order.
deps: - golang.org/x/crypto v0.52.0 → v0.56.0 (fixes GO-2026-6303/6354/6355) - golang.org/x/text v0.37.0 → v0.41.0 (fixes GO-2026-5970) - golang.org/x/sys/term bumped transitively ci (pr-validation.yml): - add explicit `go test -v -race ./...` step to the validate job - split off a `build` job (matrix: ubuntu/macos/windows) that runs after validate passes; each OS runs `go test -race`, builds the plugin with build.py, and uploads artifacts so colleagues can download and test the binaries directly from the Actions run page
…emove
- Use .bat scripts on Windows in test helpers (makeFakeBin, makeCopyBin,
makeWriteAndFailBin) so test binaries are executable without needing
shell support; use osWindows constant instead of string literal
- Close *os.File before os.Remove in pipeHeapDumpThroughRedact pre-check
to avoid Windows file-locking errors ("being used by another process")
- Strip trailing \r\n in HappyPath test to account for Windows `more`
appending CRLF
… corrupting gzip data in tests
jstall never had a --ssh-prefix flag — the code was passing an unknown option that caused "Unknown option: --ssh-prefix" for every status/jstall invocation. Fix: - Use --cf <appName> (jstall's built-in CF shortcut) for the normal case - Fall back to --ssh with the full cf-ssh command only when --app-instance-index is needed (jstall's --cf doesn't support instance selection) - Remove the --ssh-prefix argument entirely - Update embedded jstall-minimal.jar from 0.6.0 to 0.7.1 (latest) Also add missing Description fields and fix empty ShortName prefix in generateOptionsMapFromFlags so --compress/--open/--redact show proper help text instead of "-,".
- Replace --cf with --ssh "cf ssh <app> -c" so jstall doesn't internally wrap the command in `sh -c`, which fails on Windows (no sh.exe) - Fix continuation-line indentation in OPTIONS help: compute miscLineIndent so all flags align at the same column regardless of ShortName length
…ng error) jstall v0.7.2 fixed --cf to use ProcessBuilder instead of sh -c, so it now works on Windows. Switch back to --cf for the normal case (cleaner, no quoting issues). Remove shellQuote which is no longer needed. For --app-instance-index, fall back to --ssh without shell quoting since jstall also uses ProcessBuilder for --ssh in v0.7.2.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
--redact: zeros primitive arrays (byte[],char[], etc.) in the heap dump before saving (lean redaction mode), removing sensitive data like passwords and tokens. Uses the bundled hprof-redact binary.--redact-complete: zeros all primitive arrays and individual primitive fields (complete redaction, maximum privacy). Mutually exclusive with--redact.--compress: saves the dump as.hprof.gzby transferring it gzip-compressed over SSH (requires JDK 17+ on the container; falls back to uncompressed otherwise). On JDK 17+ containers the plugin also transparently usesjmap gz=1to reduce transfer size without--compress, decompressing on the fly.--open: after downloading (and optionally redacting/compressing), spins up a temporary single-serve local HTTP server on127.0.0.1and opens hprof-analyzer in the browser with?file=http://localhost:PORT/TOKEN.hprof. The server shuts down automatically after the file is fetched once.--open-url <URL>: override the hprof-analyzer base URL (e.g. a locally running instance). Implies--open.Security notes (
--open)/a3f9c2b1d4e56f78.hprof) — the real filename is never exposed127.0.0.1only (loopback, not reachable from the network)io.Copydirectly (nothttp.ServeFile) to prevent path traversal