Skip to content

Commit 036a5ea

Browse files
Add Settings and FS System Support (#24)
* enhancements: human-readable error message and hints, file system location(r/w access) support, container-dir to store dump and local-dir to save dump copy * fix issues when save local copy * Update README.md Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com> * Update cf_cli_java_plugin.go Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com> * Update cf_cli_java_plugin.go Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com> * update based on review comments * return err if utils any error * Update cf_cli_java_plugin_test.go Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com> * Update cf_cli_java_plugin_test.go Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com> * udpate according Tim's comments * fix jvmmon check issues * Update README.md Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com> * Update README.md Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com> * update per Tim's comments * support specify dump file path for jvmmon * Fix issues with OpenJDK VMs Co-authored-by: Tim Gerlach <Tim.Gerlach@sap.com>
1 parent dc73343 commit 036a5ea

File tree

10 files changed

+697
-43
lines changed

10 files changed

+697
-43
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,24 @@ OPTIONS:
8787
-app-instance-index -i [index], select to which instance of the app to connect
8888
-dry-run -n, just output to command line what would be executed
8989
-keep -k, keep the heap dump in the container; by default the heap dump will be deleted from the container's filesystem after been downloaded
90+
-container-dir -cd, the directory path in the container that the heap dump file will be saved to
91+
-local-dir -ld, the local directory path that the dump file will be saved to
9092
</pre>
9193

92-
The heap dump or thread dump (depending on what you execute) will be outputted to `std-out`.
94+
The heap dump will be copied to a local file if `-local-dir` is specified as a full folder path. Without providing `-local-dir` the heap dump will only be created in the container and not transferred.
95+
To save disk space of the application container, heap dumps are automatically deleted unless the `-keep` option is set.
96+
97+
Providing `-container-dir` is optional. If specified the plugin will create the heap dump at the given file path in the application container. Without providing this parameter, the heap dump will be created either at `/tmp` or at the file path of a file system service if attached to the container.
98+
99+
```shell
100+
cf java heap-dump [my-app] -local-dir /local/path [-container-dir /var/fspath]
101+
```
102+
103+
The thread dump will be outputted to `std-out`.
93104
You may want to redirect the command's output to file, e.g., by executing:
94105

95106
```shell
96-
cf java heap-dump [my_app] -i [my_instance_index] > heap-dump.hprof
107+
cf java thread-dump [my_app] -i [my_instance_index] > heap-dump.hprof
97108
```
98109

99110
The `-k` flag is invalid when invoking `cf java thread-dump`.

cf_cli_java_plugin.go

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"code.cloudfoundry.org/cli/cf/trace"
2121
"code.cloudfoundry.org/cli/plugin"
2222

23+
"utils"
24+
2325
guuid "github.com/satori/go.uuid"
2426
"github.com/simonleung8/flags"
2527
)
@@ -74,18 +76,18 @@ const (
7476
// user facing errors). The CLI will exit 0 if the plugin exits 0 and will exit
7577
// 1 should the plugin exit nonzero.
7678
func (c *JavaPlugin) Run(cliConnection plugin.CliConnection, args []string) {
77-
_, err := c.DoRun(&commandExecutorImpl{cliConnection: cliConnection}, &uuidGeneratorImpl{}, args)
79+
_, err := c.DoRun(&commandExecutorImpl{cliConnection: cliConnection}, &uuidGeneratorImpl{}, utils.CfJavaPluginUtilImpl{}, args)
7880
if err != nil {
7981
os.Exit(1)
8082
}
8183
}
8284

8385
// DoRun is an internal method that we use to wrap the cmd package with CommandExecutor for test purposes
84-
func (c *JavaPlugin) DoRun(commandExecutor cmd.CommandExecutor, uuidGenerator uuid.UUIDGenerator, args []string) (string, error) {
86+
func (c *JavaPlugin) DoRun(commandExecutor cmd.CommandExecutor, uuidGenerator uuid.UUIDGenerator, util utils.CfJavaPluginUtilImpl, args []string) (string, error) {
8587
traceLogger := trace.NewLogger(os.Stdout, true, os.Getenv("CF_TRACE"), "")
8688
ui := terminal.NewUI(os.Stdin, os.Stdout, terminal.NewTeePrinter(os.Stdout), traceLogger)
8789

88-
output, err := c.execute(commandExecutor, uuidGenerator, args)
90+
output, err := c.execute(commandExecutor, uuidGenerator, util, args)
8991
if err != nil {
9092
ui.Failed(err.Error())
9193

@@ -101,7 +103,7 @@ func (c *JavaPlugin) DoRun(commandExecutor cmd.CommandExecutor, uuidGenerator uu
101103
return output, err
102104
}
103105

104-
func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator uuid.UUIDGenerator, args []string) (string, error) {
106+
func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator uuid.UUIDGenerator, util utils.CfJavaPluginUtilImpl, args []string) (string, error) {
105107
if len(args) == 0 {
106108
return "", &InvalidUsageError{message: "No command provided"}
107109
}
@@ -125,6 +127,8 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
125127
commandFlags.NewIntFlagWithDefault("app-instance-index", "i", "application `instance` to connect to", -1)
126128
commandFlags.NewBoolFlag("keep", "k", "whether to `keep` the heap/thread-dump on the container of the application instance after having downloaded it locally")
127129
commandFlags.NewBoolFlag("dry-run", "n", "triggers the `dry-run` mode to show only the cf-ssh command that would have been executed")
130+
commandFlags.NewStringFlag("container-dir", "cd", "specify the folder path where the dump file should be stored in the container")
131+
commandFlags.NewStringFlag("local-dir", "ld", "specify the folder where the dump file will be downloaded to, dump file wil not be copied to local if this parameter was not set")
128132

129133
parseErr := commandFlags.Parse(args[1:]...)
130134
if parseErr != nil {
@@ -134,6 +138,11 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
134138
applicationInstance := commandFlags.Int("app-instance-index")
135139
keepAfterDownload := commandFlags.IsSet("keep")
136140

141+
remoteDir := commandFlags.String("container-dir")
142+
localDir := commandFlags.String("local-dir")
143+
144+
copyToLocal := len(localDir) > 0
145+
137146
arguments := commandFlags.Args()
138147
argumentLen := len(arguments)
139148

@@ -149,7 +158,12 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
149158
if commandFlags.IsSet("keep") {
150159
return "", &InvalidUsageError{message: fmt.Sprintf("The flag %q is not supported for thread-dumps", "keep")}
151160
}
152-
break
161+
if commandFlags.IsSet("container-dir") {
162+
return "", &InvalidUsageError{message: fmt.Sprintf("The flag %q is not supported for thread-dumps", "container-dir")}
163+
}
164+
if commandFlags.IsSet("local-dir") {
165+
return "", &InvalidUsageError{message: fmt.Sprintf("The flag %q is not supported for thread-dumps", "local-dir")}
166+
}
153167
default:
154168
return "", &InvalidUsageError{message: fmt.Sprintf("Unrecognized command %q: supported commands are 'heap-dump' and 'thread-dump' (see cf help)", command)}
155169
}
@@ -168,10 +182,22 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
168182
}
169183

170184
var remoteCommandTokens = []string{JavaDetectionCommand}
171-
185+
heapdumpFileName := ""
186+
fspath := remoteDir
172187
switch command {
173188
case heapDumpCommand:
174-
heapdumpFileName := "/tmp/heapdump-" + uuidGenerator.Generate() + ".hprof"
189+
190+
supported, err := util.CheckRequiredTools(applicationName)
191+
if err != nil || !supported {
192+
return "required tools checking failed", err
193+
}
194+
195+
fspath, err = util.GetAvailablePath(applicationName, remoteDir)
196+
if err != nil {
197+
return "", err
198+
}
199+
heapdumpFileName = fspath + "/" + applicationName + "-heapdump-" + uuidGenerator.Generate() + ".hprof"
200+
175201
remoteCommandTokens = append(remoteCommandTokens,
176202
// Check file does not already exist
177203
"if [ -f "+heapdumpFileName+" ]; then echo >&2 'Heap dump "+heapdumpFileName+" already exists'; exit 1; fi",
@@ -185,30 +211,22 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
185211
*/
186212
// OpenJDK: Wrap everything in an if statement in case jmap is available
187213
"JMAP_COMMAND=`find -executable -name jmap | head -1 | tr -d [:space:]`",
214+
// SAP JVM: Wrap everything in an if statement in case jvmmon is available
215+
"JVMMON_COMMAND=`find -executable -name jvmmon | head -1 | tr -d [:space:]`",
188216
"if [ -n \"${JMAP_COMMAND}\" ]; then true",
189217
"OUTPUT=$( ${JMAP_COMMAND} -dump:format=b,file="+heapdumpFileName+" $(pidof java) ) || STATUS_CODE=$?",
190218
"if [ ! -s "+heapdumpFileName+" ]; then echo >&2 ${OUTPUT}; exit 1; fi",
191219
"if [ ${STATUS_CODE:-0} -gt 0 ]; then echo >&2 ${OUTPUT}; exit ${STATUS_CODE}; fi",
192-
"cat "+heapdumpFileName,
193-
"exit 0",
194-
"fi",
195-
// SAP JVM: Wrap everything in an if statement in case jvmmon is available
196-
"JVMMON_COMMAND=`find -executable -name jvmmon | head -1 | tr -d [:space:]`",
197-
"if [ -n \"${JVMMON_COMMAND}\" ]; then true",
198-
"OUTPUT=$( ${JVMMON_COMMAND} -pid $(pidof java) -c \"dump heap\" ) || STATUS_CODE=$?",
220+
"elif [ -n \"${JVMMON_COMMAND}\" ]; then true",
221+
"echo -e 'change command line flag flags=-XX:HeapDumpOnDemandPath="+fspath+"\ndump heap' > setHeapDumpOnDemandPath.sh",
222+
"OUTPUT=$( ${JVMMON_COMMAND} -pid $(pidof java) -cmd \"setHeapDumpOnDemandPath.sh\" ) || STATUS_CODE=$?",
199223
"sleep 5", // Writing the heap dump is triggered asynchronously -> give the jvm some time to create the file
200-
"HEAP_DUMP_NAME=`find -name 'java_pid*.hprof' -printf '%T@ %p\\0' | sort -zk 1nr | sed -z 's/^[^ ]* //' | tr '\\0' '\\n' | head -n 1`",
201-
"SIZE=-1; OLD_SIZE=$(stat -c '%s' \"${HEAP_DUMP_NAME}\"); while [ \"${SIZE}\" != \"${OLD_SIZE}\" ]; do sleep 3; SIZE=$(stat -c '%s' \"${HEAP_DUMP_NAME}\"); done",
224+
"HEAP_DUMP_NAME=`find "+fspath+" -name 'java_pid*.hprof' -printf '%T@ %p\\0' | sort -zk 1nr | sed -z 's/^[^ ]* //' | tr '\\0' '\\n' | head -n 1`",
225+
"SIZE=-1; OLD_SIZE=$(stat -c '%s' \"${HEAP_DUMP_NAME}\"); while [ ${SIZE} != ${OLD_SIZE} ]; do OLD_SIZE=${SIZE}; sleep 3; SIZE=$(stat -c '%s' \"${HEAP_DUMP_NAME}\"); done",
202226
"if [ ! -s \"${HEAP_DUMP_NAME}\" ]; then echo >&2 ${OUTPUT}; exit 1; fi",
203227
"if [ ${STATUS_CODE:-0} -gt 0 ]; then echo >&2 ${OUTPUT}; exit ${STATUS_CODE}; fi",
204-
"cat ${HEAP_DUMP_NAME}",
205228
"fi")
206-
if !keepAfterDownload {
207-
// OpenJDK
208-
remoteCommandTokens = append(remoteCommandTokens, "rm -f "+heapdumpFileName)
209-
// SAP JVM
210-
remoteCommandTokens = append(remoteCommandTokens, "if [ -n \"${HEAP_DUMP_NAME}\" ]; then rm -f ${HEAP_DUMP_NAME} ${HEAP_DUMP_NAME%.*}.addons; fi")
211-
}
229+
212230
case threadDumpCommand:
213231
// OpenJDK
214232
remoteCommandTokens = append(remoteCommandTokens, "JSTACK_COMMAND=`find -executable -name jstack | head -1`; if [ -n \"${JSTACK_COMMAND}\" ]; then ${JSTACK_COMMAND} $(pidof java); exit 0; fi")
@@ -229,7 +247,40 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
229247
cfSSHArguments = append(cfSSHArguments, remoteCommand)
230248

231249
output, err := commandExecutor.Execute(cfSSHArguments)
250+
if command == heapDumpCommand {
251+
252+
finalFile, err := util.FindDumpFile(applicationName, heapdumpFileName, fspath)
253+
if err == nil && finalFile != "" {
254+
heapdumpFileName = finalFile
255+
fmt.Println("Successfully created heap dump in application container at: " + heapdumpFileName)
256+
} else {
257+
fmt.Println("Failed to find heap dump in application container")
258+
fmt.Println(finalFile)
259+
fmt.Println(heapdumpFileName)
260+
fmt.Println(fspath)
261+
return "", err
262+
}
263+
264+
if copyToLocal {
265+
localFileFullPath := localDir + "/" + applicationName + "-heapdump-" + uuidGenerator.Generate() + ".hprof"
266+
err = util.CopyOverCat(applicationName, heapdumpFileName, localFileFullPath)
267+
if err == nil {
268+
fmt.Println("Heap dump filed saved to: " + localFileFullPath)
269+
} else {
270+
return "", err
271+
}
272+
} else {
273+
fmt.Println("Heap dump will not be copied as parameter `local-dir` was not set")
274+
}
232275

276+
if !keepAfterDownload {
277+
err = util.DeleteRemoteFile(applicationName, heapdumpFileName)
278+
if err != nil {
279+
return "", err
280+
}
281+
fmt.Println("Heap dump filed deleted in app container")
282+
}
283+
}
233284
// We keep this around to make the compiler happy, but commandExecutor.Execute will cause an os.Exit
234285
return strings.Join(output, "\n"), err
235286
}
@@ -262,7 +313,7 @@ func (c *JavaPlugin) GetMetadata() plugin.PluginMetadata {
262313
Commands: []plugin.Command{
263314
{
264315
Name: "java",
265-
HelpText: "Obtain a heap-dump or thread-dump from a running, Diego-enabled, SSH-enabled Java application.",
316+
HelpText: "Obtain a heap-dump or thread-dump from a running, SSH-enabled Java application.",
266317

267318
// UsageDetails is optional
268319
// It is used to show help of usage of each command
@@ -272,6 +323,8 @@ func (c *JavaPlugin) GetMetadata() plugin.PluginMetadata {
272323
"app-instance-index": "-i [index], select to which instance of the app to connect",
273324
"keep": "-k, keep the heap dump in the container; by default the heap dump will be deleted from the container's filesystem after been downloaded",
274325
"dry-run": "-n, just output to command line what would be executed",
326+
"container-dir": "-cd, the directory path in the container that the heap dump file will be saved to",
327+
"local-dir": "-ld, the local directory path that the dump file will be saved to",
275328
},
276329
},
277330
},

0 commit comments

Comments
 (0)