Skip to content

Commit f456da5

Browse files
committed
Update heap dump capture for compressed file to use the original extension
1 parent 634fc90 commit f456da5

File tree

1 file changed

+133
-121
lines changed

1 file changed

+133
-121
lines changed

internal/capture/heap.go

Lines changed: 133 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ var compressedHeapExtensions = []string{
4343
const hdOut = "heap_dump.out"
4444
const hdZip = "heap_dump.zip"
4545

46+
type HeapDumpFile struct {
47+
SrcHeapDumpPath string
48+
SrcHeapDumpFile *os.File
49+
50+
DstCompressedPath string
51+
DstCompressedFile *os.File
52+
}
53+
54+
func (h *HeapDumpFile) IsSrcHeapDumpAlreadyCompressed() bool {
55+
ext := strings.TrimPrefix(filepath.Ext(h.SrcHeapDumpPath), ".")
56+
57+
for _, compressedExt := range compressedHeapExtensions {
58+
if ext == compressedExt {
59+
return true
60+
}
61+
}
62+
return false
63+
}
64+
4665
type HeapDump struct {
4766
Capture
4867
JavaHome string
@@ -61,84 +80,120 @@ func NewHeapDump(javaHome string, pid int, hdPath string, dump bool) *HeapDump {
6180
}
6281
}
6382

64-
// getDestHeapDumpFilename returns the appropriate filename based on compression status
65-
func (t *HeapDump) getDestHeapDumpFilename(isCompressed bool) string {
66-
if isCompressed {
67-
return hdZip
68-
}
69-
return hdOut
70-
}
71-
72-
// Run executes the heap dump capture process and uploads the captured file
73-
// to the specified endpoint.
7483
func (t *HeapDump) Run() (Result, error) {
75-
var hd *os.File
76-
var err error
77-
var isCompressed bool
78-
var contentEncoding string
84+
heapDumpFile := &HeapDumpFile{}
7985

80-
// Try pre-captured heap dump first
8186
if len(t.hdPath) > 0 {
82-
isCompressed, contentEncoding = isCompressedHeapFile(t.hdPath)
83-
if isCompressed {
84-
logger.Log("detected pre-compressed heap dump file: %s with encoding: %s", t.hdPath, contentEncoding)
87+
heapDumpFile.SrcHeapDumpPath = t.hdPath
88+
89+
hd, err := os.Open(heapDumpFile.SrcHeapDumpPath)
90+
91+
// Fallback, try to open the file in the Docker container
92+
if err != nil && runtime.GOOS == "linux" {
93+
logger.Log("failed to open hdPath(%s) err: %s. Trying to open in the Docker container...", t.hdPath, err.Error())
94+
hd, err = os.Open(filepath.Join("/proc", strconv.Itoa(t.Pid), "root", t.hdPath))
8595
}
8696

87-
hd, err = t.getPreCapturedDumpFile(isCompressed)
8897
if err != nil {
98+
logger.Log("failed to open hdPath(%s) err: %s", t.hdPath, err.Error())
8999
return Result{
90-
Msg: fmt.Sprintf("capture heap dump failed: %s", err.Error()),
100+
Msg: fmt.Sprintf("failed to open heap dump file: %s", err.Error()),
91101
Ok: false,
92-
}, nil
102+
}, err
93103
}
104+
105+
heapDumpFile.SrcHeapDumpFile = hd
106+
107+
// Ensure the source heap dump file is closed when the function exits
108+
defer func() {
109+
if err := hd.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
110+
logger.Log("failed to close source heap dump file: %s", err.Error())
111+
}
112+
}()
94113
} else if t.Pid > 0 && t.dump {
95-
var actualDumpPath string
96-
// Then try capturing a new heap dump
97-
hd, actualDumpPath, err = t.captureDumpFile()
114+
hd, actualDumpPath, err := t.captureDumpFile()
98115
if err != nil {
99116
return Result{
100117
Msg: fmt.Sprintf("capture heap dump failed: %s", err.Error()),
101118
Ok: false,
102119
}, nil
103120
}
104121

105-
// Cleanup the actual dump file after we're done with it
106-
if actualDumpPath != "" {
107-
defer func() {
108-
err := os.Remove(actualDumpPath)
109-
if err != nil {
110-
logger.Trace().Err(err).Str("file", actualDumpPath).Msg("failed to rm hd file")
111-
}
112-
}()
113-
}
122+
heapDumpFile.SrcHeapDumpPath = actualDumpPath
123+
heapDumpFile.SrcHeapDumpFile = hd
124+
125+
// Ensure the captured heap dump file is closed when the function exits
126+
defer func() {
127+
if err := hd.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
128+
logger.Log("failed to close captured heap dump file: %s", err.Error())
129+
}
130+
}()
131+
132+
// because this code creates the file, it's responsible for cleaning it up
133+
defer func() {
134+
if removeErr := os.Remove(actualDumpPath); removeErr != nil {
135+
logger.Log("failed to rm hd file %s cause err: %s", actualDumpPath, removeErr.Error())
136+
}
137+
}()
114138
}
115139

116-
if hd == nil {
140+
if heapDumpFile.SrcHeapDumpFile == nil {
117141
return Result{Msg: "skipped heap dump"}, nil
118142
}
119143

120-
tempFilename := t.getDestHeapDumpFilename(isCompressed)
144+
if heapDumpFile.IsSrcHeapDumpAlreadyCompressed() {
145+
logger.Log("copying heap dump data %s", t.hdPath)
146+
147+
srcExtension := strings.TrimPrefix(filepath.Ext(heapDumpFile.SrcHeapDumpPath), ".")
148+
heapDumpFile.DstCompressedPath = "heap_dump." + srcExtension
121149

122-
defer func() {
123-
err := hd.Close()
124-
if err != nil && !errors.Is(err, os.ErrClosed) {
125-
logger.Log("failed to close hd file %s cause err: %s", tempFilename, err.Error())
150+
dstCompressedFile, err := os.OpenFile(heapDumpFile.DstCompressedPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
151+
if err != nil {
152+
return Result{
153+
Msg: fmt.Sprintf("failed creating heap dump in current working directory: %s", err.Error()),
154+
Ok: false,
155+
}, nil
126156
}
127-
}()
128157

129-
var fileToUpload *os.File
130-
var uploadContentEncoding string
158+
// Ensure the file is closed when the function exits
159+
defer func() {
160+
if err := dstCompressedFile.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
161+
logger.Log("failed to close destination compressed file: %s", err.Error())
162+
}
163+
}()
164+
165+
_, err = io.Copy(dstCompressedFile, heapDumpFile.SrcHeapDumpFile)
166+
if err != nil {
167+
return Result{
168+
Msg: fmt.Sprintf("failed copying heap dump data: %s", err.Error()),
169+
Ok: false,
170+
}, nil
171+
}
131172

132-
if isCompressed {
133-
// If the file is already compressed, use it directly without re-compressing
134-
logger.Log("file is already compressed, skipping compression step")
135-
fileToUpload = hd
136-
uploadContentEncoding = contentEncoding
173+
// Sync the file to ensure all data is written to disk
174+
err = dstCompressedFile.Sync()
175+
if err != nil {
176+
return Result{
177+
Msg: fmt.Sprintf("failed syncing heap dump file: %s", err.Error()),
178+
Ok: false,
179+
}, nil
180+
}
181+
182+
// Reset file position to beginning for reading during upload
183+
_, err = dstCompressedFile.Seek(0, 0)
184+
if err != nil {
185+
return Result{
186+
Msg: fmt.Sprintf("failed seeking to beginning of heap dump file: %s", err.Error()),
187+
Ok: false,
188+
}, nil
189+
}
190+
191+
heapDumpFile.DstCompressedFile = dstCompressedFile
192+
logger.Log("copied heap dump data %s to %s", t.hdPath, heapDumpFile.DstCompressedPath)
137193
} else {
138-
// For uncompressed files, compress them
139194
logger.Log("captured heap dump data, zipping...")
140195

141-
zipfile, err := t.CreateZipFile(hd)
196+
zipfile, err := t.CreateZipFile(heapDumpFile.SrcHeapDumpFile)
142197
if err != nil {
143198
return Result{
144199
Msg: fmt.Sprintf("capture heap dump failed: %s", err.Error()),
@@ -152,65 +207,16 @@ func (t *HeapDump) Run() (Result, error) {
152207
}
153208
}()
154209

155-
defer func() {
156-
err = os.Remove(tempFilename)
157-
if err != nil {
158-
logger.Log("failed to rm hd file %s cause err: %s", tempFilename, err.Error())
159-
}
160-
}()
161-
162-
fileToUpload = zipfile
163-
uploadContentEncoding = "zip"
210+
heapDumpFile.DstCompressedPath = hdZip
211+
heapDumpFile.DstCompressedFile = zipfile
164212
}
165213

166-
result := t.UploadCapturedFile(fileToUpload, uploadContentEncoding)
214+
// Upload heapDumpFile.DstCompressedFile to the endpoint
215+
dstCompressedExtension := strings.TrimPrefix(filepath.Ext(heapDumpFile.DstCompressedPath), ".")
216+
result := t.UploadCapturedFile(heapDumpFile.DstCompressedFile, dstCompressedExtension)
167217
return result, nil
168218
}
169219

170-
// getPreCapturedDumpFile handles the case when a heap dump is pre-captured (using the hdPath field)
171-
func (t *HeapDump) getPreCapturedDumpFile(isCompressed bool) (*os.File, error) {
172-
hdf, err := os.Open(t.hdPath)
173-
174-
// Fallback, try to open the file in the Docker container
175-
if err != nil && runtime.GOOS == "linux" {
176-
logger.Log("failed to open hdPath(%s) err: %s. Trying to open in the Docker container...", t.hdPath, err.Error())
177-
hdf, err = os.Open(filepath.Join("/proc", strconv.Itoa(t.Pid), "root", t.hdPath))
178-
}
179-
180-
if err != nil {
181-
logger.Log("failed to open hdPath(%s) err: %s", t.hdPath, err.Error())
182-
return nil, err
183-
}
184-
185-
logger.Log("copying heap dump data %s", t.hdPath)
186-
187-
defer func() {
188-
err := hdf.Close()
189-
if err != nil {
190-
logger.Log("failed to close hd file %s cause err: %s", t.hdPath, err.Error())
191-
}
192-
}()
193-
194-
filename := t.getDestHeapDumpFilename(isCompressed)
195-
hd, err := os.Create(filename)
196-
if err != nil {
197-
return nil, err
198-
}
199-
200-
_, err = io.Copy(hd, hdf)
201-
if err != nil {
202-
return nil, err
203-
}
204-
205-
_, err = hd.Seek(0, 0)
206-
if err != nil {
207-
return nil, err
208-
}
209-
210-
logger.Log("copied heap dump data %s to %s", t.hdPath, filename)
211-
return hd, nil
212-
}
213-
214220
// captureDumpFile handles the case when a heap dump needs to be captured (using the Pid field)
215221
// and returns both the file handle and the actual dump path
216222
func (t *HeapDump) captureDumpFile() (*os.File, string, error) {
@@ -255,25 +261,44 @@ func (t *HeapDump) CreateZipFile(hd *os.File) (*os.File, error) {
255261
return nil, fmt.Errorf("failed to create zip file: %w", err)
256262
}
257263

258-
writer := zip.NewWriter(bufio.NewWriter(zipfile))
264+
bufferedWriter := bufio.NewWriter(zipfile)
265+
writer := zip.NewWriter(bufferedWriter)
259266
out, err := writer.Create(hdOut)
260267
if err != nil {
268+
zipfile.Close()
261269
return nil, fmt.Errorf("failed to create zip file: %w", err)
262270
}
263271

264272
_, err = io.Copy(out, hd)
265273
if err != nil {
274+
zipfile.Close()
266275
return nil, fmt.Errorf("failed to zip heap dump file: %w", err)
267276
}
268277

278+
// Close zip writer first to write central directory records
269279
err = writer.Close()
270280
if err != nil {
271-
return nil, fmt.Errorf("failed to finish zipping heap dump file: %w", err)
281+
zipfile.Close()
282+
return nil, fmt.Errorf("failed to close zip writer: %w", err)
283+
}
284+
285+
// Flush the buffered writer to ensure all data is written to the file
286+
err = bufferedWriter.Flush()
287+
if err != nil {
288+
zipfile.Close()
289+
return nil, fmt.Errorf("failed to flush zip file buffer: %w", err)
290+
}
291+
292+
// Close the file to ensure all data is synced to disk
293+
err = zipfile.Close()
294+
if err != nil {
295+
return nil, fmt.Errorf("failed to close zip file: %w", err)
272296
}
273297

274-
e := zipfile.Sync()
275-
if e != nil && !errors.Is(e, os.ErrClosed) {
276-
logger.Log("failed to sync file %s", e)
298+
// Reopen the file for reading to pass to the upload function
299+
zipfile, err = os.Open(hdZip)
300+
if err != nil {
301+
return nil, fmt.Errorf("failed to reopen zip file for upload: %w", err)
277302
}
278303

279304
return zipfile, nil
@@ -288,19 +313,6 @@ func (t *HeapDump) UploadCapturedFile(file *os.File, contentEncoding string) Res
288313
}
289314
}
290315

291-
// isCompressedHeapFile checks if the given file is a compressed heap dump file
292-
// and returns the extension as the MIME type if it is.
293-
func isCompressedHeapFile(filePath string) (bool, string) {
294-
ext := strings.TrimPrefix(filepath.Ext(filePath), ".")
295-
296-
for _, compressedExt := range compressedHeapExtensions {
297-
if ext == compressedExt {
298-
return true, ext
299-
}
300-
}
301-
return false, ext
302-
}
303-
304316
// heapDump runs the JDK tool (jcmd, jattach, etc) to capture the heap dump to the requested file.
305317
// The returned actualDumpPath is the actual file name written to is returned.
306318
// In IBM JDK, this may not be the same as the requested filename for several reasons:

0 commit comments

Comments
 (0)