Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin enhancement #24

Merged
merged 16 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,21 @@ OPTIONS:
-app-instance-index -i [index], select to which instance of the app to connect
-dry-run -n, just output to command line what would be executed
-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
-container-dir -cd, the directory path in the container that the heap dump file will be saved to
-local-dir -ld, the local directory path that the dump file will be saved to
</pre>

The heap dump or thread dump (depending on what you execute) will be outputted to `std-out`.
The heap dump will be copied if `-local-dir` is specified (should be full folder path):
zhouwei0115 marked this conversation as resolved.
Show resolved Hide resolved

```shell
cf java heap-dump [my-app] --container-dir /var/fspath --local-dir /local/path
zhouwei0115 marked this conversation as resolved.
Show resolved Hide resolved
```

The thread dump will be outputted to `std-out`.
zhouwei0115 marked this conversation as resolved.
Show resolved Hide resolved
You may want to redirect the command's output to file, e.g., by executing:

```shell
cf java heap-dump [my_app] -i [my_instance_index] > heap-dump.hprof
cf java thread-dump [my_app] -i [my_instance_index] > heap-dump.hprof
```

The `-k` flag is invalid when invoking `cf java thread-dump`.
Expand Down
86 changes: 65 additions & 21 deletions cf_cli_java_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"code.cloudfoundry.org/cli/cf/trace"
"code.cloudfoundry.org/cli/plugin"

"utils"

guuid "github.com/satori/go.uuid"
"github.com/simonleung8/flags"
)
Expand Down Expand Up @@ -74,18 +76,18 @@ const (
// user facing errors). The CLI will exit 0 if the plugin exits 0 and will exit
// 1 should the plugin exit nonzero.
func (c *JavaPlugin) Run(cliConnection plugin.CliConnection, args []string) {
_, err := c.DoRun(&commandExecutorImpl{cliConnection: cliConnection}, &uuidGeneratorImpl{}, args)
_, err := c.DoRun(&commandExecutorImpl{cliConnection: cliConnection}, &uuidGeneratorImpl{}, utils.CfJavaPluginUtilImpl{}, args)
if err != nil {
os.Exit(1)
}
}

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

output, err := c.execute(commandExecutor, uuidGenerator, args)
output, err := c.execute(commandExecutor, uuidGenerator, util, args)
if err != nil {
ui.Failed(err.Error())

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

func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator uuid.UUIDGenerator, args []string) (string, error) {
func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator uuid.UUIDGenerator, util utils.CfJavaPluginUtilImpl, args []string) (string, error) {
if len(args) == 0 {
return "", &InvalidUsageError{message: "No command provided"}
}
Expand All @@ -125,6 +127,8 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
commandFlags.NewIntFlagWithDefault("app-instance-index", "i", "application `instance` to connect to", -1)
commandFlags.NewBoolFlag("keep", "k", "whether to `keep` the heap/thread-dump on the container of the application instance after having downloaded it locally")
commandFlags.NewBoolFlag("dry-run", "n", "triggers the `dry-run` mode to show only the cf-ssh command that would have been executed")
commandFlags.NewStringFlag("container-dir", "cd", "specify the folder path where the dump file should be stored in the container")
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")

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

remoteDir := commandFlags.String("container-dir")
localDir := commandFlags.String("local-dir")

copyToLocal := len(localDir) > 0

arguments := commandFlags.Args()
argumentLen := len(arguments)

Expand All @@ -149,7 +158,12 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
if commandFlags.IsSet("keep") {
return "", &InvalidUsageError{message: fmt.Sprintf("The flag %q is not supported for thread-dumps", "keep")}
}
break
if commandFlags.IsSet("container-dir") {
return "", &InvalidUsageError{message: fmt.Sprintf("The flag %q is not supported for thread-dumps", "container-dir")}
}
if commandFlags.IsSet("local-dir") {
return "", &InvalidUsageError{message: fmt.Sprintf("The flag %q is not supported for thread-dumps", "local-dir")}
}
default:
return "", &InvalidUsageError{message: fmt.Sprintf("Unrecognized command %q: supported commands are 'heap-dump' and 'thread-dump' (see cf help)", command)}
}
Expand All @@ -168,10 +182,21 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
}

var remoteCommandTokens = []string{JavaDetectionCommand}

heapdumpFileName := ""
switch command {
case heapDumpCommand:
heapdumpFileName := "/tmp/heapdump-" + uuidGenerator.Generate() + ".hprof"

supported, err := util.CheckRequiredTools(applicationName)
if err != nil || !supported {
return "required tools checking failed", err
}

fspath, err := util.GetAvailablePath(applicationName, remoteDir)
if err != nil {
return "", err
}
heapdumpFileName = fspath + "/" + applicationName + "-heapdump-" + uuidGenerator.Generate() + ".hprof"

remoteCommandTokens = append(remoteCommandTokens,
// Check file does not already exist
"if [ -f "+heapdumpFileName+" ]; then echo >&2 'Heap dump "+heapdumpFileName+" already exists'; exit 1; fi",
Expand All @@ -185,30 +210,21 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
*/
// OpenJDK: Wrap everything in an if statement in case jmap is available
"JMAP_COMMAND=`find -executable -name jmap | head -1 | tr -d [:space:]`",
// SAP JVM: Wrap everything in an if statement in case jvmmon is available
"JVMMON_COMMAND=`find -executable -name jvmmon | head -1 | tr -d [:space:]`",
"if [ -n \"${JMAP_COMMAND}\" ]; then true",
"OUTPUT=$( ${JMAP_COMMAND} -dump:format=b,file="+heapdumpFileName+" $(pidof java) ) || STATUS_CODE=$?",
"if [ ! -s "+heapdumpFileName+" ]; then echo >&2 ${OUTPUT}; exit 1; fi",
"if [ ${STATUS_CODE:-0} -gt 0 ]; then echo >&2 ${OUTPUT}; exit ${STATUS_CODE}; fi",
"cat "+heapdumpFileName,
"exit 0",
"fi",
// SAP JVM: Wrap everything in an if statement in case jvmmon is available
"JVMMON_COMMAND=`find -executable -name jvmmon | head -1 | tr -d [:space:]`",
"if [ -n \"${JVMMON_COMMAND}\" ]; then true",
"elif [ -n \"${JVMMON_COMMAND}\" ]; then true",
"OUTPUT=$( ${JVMMON_COMMAND} -pid $(pidof java) -c \"dump heap\" ) || STATUS_CODE=$?",
"sleep 5", // Writing the heap dump is triggered asynchronously -> give the jvm some time to create the file
"HEAP_DUMP_NAME=`find -name 'java_pid*.hprof' -printf '%T@ %p\\0' | sort -zk 1nr | sed -z 's/^[^ ]* //' | tr '\\0' '\\n' | head -n 1`",
"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",
"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",
"if [ ! -s \"${HEAP_DUMP_NAME}\" ]; then echo >&2 ${OUTPUT}; exit 1; fi",
"if [ ${STATUS_CODE:-0} -gt 0 ]; then echo >&2 ${OUTPUT}; exit ${STATUS_CODE}; fi",
"cat ${HEAP_DUMP_NAME}",
"fi")
if !keepAfterDownload {
// OpenJDK
remoteCommandTokens = append(remoteCommandTokens, "rm -f "+heapdumpFileName)
// SAP JVM
remoteCommandTokens = append(remoteCommandTokens, "if [ -n \"${HEAP_DUMP_NAME}\" ]; then rm -f ${HEAP_DUMP_NAME} ${HEAP_DUMP_NAME%.*}.addons; fi")
}

case threadDumpCommand:
// OpenJDK
remoteCommandTokens = append(remoteCommandTokens, "JSTACK_COMMAND=`find -executable -name jstack | head -1`; if [ -n \"${JSTACK_COMMAND}\" ]; then ${JSTACK_COMMAND} $(pidof java); exit 0; fi")
Expand All @@ -229,7 +245,33 @@ func (c *JavaPlugin) execute(commandExecutor cmd.CommandExecutor, uuidGenerator
cfSSHArguments = append(cfSSHArguments, remoteCommand)

output, err := commandExecutor.Execute(cfSSHArguments)
if command == heapDumpCommand {

finalFile, err := util.FindDumpFile(applicationName, heapdumpFileName)
zhouwei0115 marked this conversation as resolved.
Show resolved Hide resolved
if err == nil && finalFile != "" {
heapdumpFileName = finalFile
}

TimGerlach marked this conversation as resolved.
Show resolved Hide resolved
if copyToLocal {
localFileFullPath := localDir + "/" + applicationName + "-heapdump-" + uuidGenerator.Generate() + ".hprof"
err = util.CopyOverCat(applicationName, heapdumpFileName, localFileFullPath)
if err == nil {
fmt.Println("heap dump filed saved to: " + localFileFullPath)
} else {
return "", err
}
} else {
fmt.Println("heap dump will not be copied as parameter `local-dir` was not set")
}
zhouwei0115 marked this conversation as resolved.
Show resolved Hide resolved

if !keepAfterDownload {
err = util.DeleteRemoteFile(applicationName, heapdumpFileName)
if err != nil {
return "", err
zhouwei0115 marked this conversation as resolved.
Show resolved Hide resolved
}
fmt.Println("heap dump filed deleted in app container")
}
}
// We keep this around to make the compiler happy, but commandExecutor.Execute will cause an os.Exit
return strings.Join(output, "\n"), err
}
Expand Down Expand Up @@ -272,6 +314,8 @@ func (c *JavaPlugin) GetMetadata() plugin.PluginMetadata {
"app-instance-index": "-i [index], select to which instance of the app to connect",
"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",
"dry-run": "-n, just output to command line what would be executed",
"container-dir": "-cd, the directory path in the container that the heap dump file will be saved to",
"local-dir": "-ld, the local directory path that the dump file will be saved to",
},
},
},
Expand Down
Loading