From fcb426e4bf5e163b20afaa15967bca3aa630b86d Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Sun, 28 Aug 2022 19:01:29 +0000 Subject: [PATCH] jb: allow backend debugging running in preview env --- .../ide/jetbrains/backend-plugin/README.md | 10 +- .../jetbrains/backend-plugin/remote-debug.sh | 48 +++++++ components/ide/jetbrains/image/status/main.go | 123 +++++++++++++----- .../ide/jetbrains/image/status/main_test.go | 8 +- 4 files changed, 155 insertions(+), 34 deletions(-) create mode 100755 components/ide/jetbrains/backend-plugin/remote-debug.sh diff --git a/components/ide/jetbrains/backend-plugin/README.md b/components/ide/jetbrains/backend-plugin/README.md index 9f51df8e5dc25f..cda12c894be97a 100644 --- a/components/ide/jetbrains/backend-plugin/README.md +++ b/components/ide/jetbrains/backend-plugin/README.md @@ -12,6 +12,8 @@ IntelliJ delivers better experience for development of JetBrains plugins. We sho issues [here](https://youtrack.jetbrains.com/issues?q=project:%20CWM) under remote development subsystem. + + ### Local Usually you will need to create a preview environments to try your changes, but if your changes don't touch any other components beside the backend plugin then you can test against the running workspace: @@ -50,7 +52,7 @@ Run `./hot-deploy.sh (latest|stable)` to build and publish the backend plugin im update the IDE config map in a preview environment. After that start a new workspace in preview environment with corresponding version to try your changes. -### Hot swap +### Hot swapping Run `./hot-swap.sh ` to build a new backend plugin version corresponding to a workspace running in preview environment, install a new version in such workspace and restart the JB backend. Reconnect to the restarted JB backend to try new changes. @@ -59,3 +61,9 @@ If you need to change the startup endpoint then run to hot swap it too: ```bash leeway build components/ide/jetbrains/image/status:hot-swap -DworkspaceUrl= ``` + +### Remote debugging + +Run `./remote-debug.sh ()?` to configure remote debugging in a workpace running in preview environment. +It will configure remote debug port, restart the backend and start port forwarding in your dev workspace. +Create a new `Remote JVM Debug` launch configuration with the forwarded port and launch it. diff --git a/components/ide/jetbrains/backend-plugin/remote-debug.sh b/components/ide/jetbrains/backend-plugin/remote-debug.sh new file mode 100755 index 00000000000000..36a81c899906dd --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/remote-debug.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Copyright (c) 2022 Gitpod GmbH. All rights reserved. +# Licensed under the GNU Affero General Public License (AGPL). +# See License-AGPL.txt in the project root for license information. + +# This script configure remote debugging in a workspace running in a preview environment. +# It updates VM options with remote debug agent, restart the JB backend to apply them, +# and start port forwarding of the remote debug port. You can configure `Remote JVM Debug` +# run configuration using the forwarded port. +# +# ./remote-debug.sh ()? + +workspaceUrl=${1-} +[ -z "$workspaceUrl" ] && echo "Please provide a workspace URL as first argument." && exit 1 +workspaceUrl=$(echo "$workspaceUrl" |sed -e "s/\/$//") +echo "URL: $workspaceUrl" + +workspaceDesc=$(gpctl workspaces describe "$workspaceUrl" -o=json) + +podName=$(echo "$workspaceDesc" | jq .runtime.pod_name -r) +echo "Pod: $podName" + +workspaceId=$(echo "$workspaceDesc" | jq .metadata.meta_id -r) +echo "ID: $workspaceId" + +clusterHost=$(kubectl exec -it "$podName" -- printenv GITPOD_WORKSPACE_CLUSTER_HOST |sed -e "s/\s//g") +echo "Cluster Host: $clusterHost" + +# prepare ssh +ownerToken=$(kubectl get pod "$podName" -o=json | jq ".metadata.annotations.\"gitpod\/ownerToken\"" -r) +sshConfig="/tmp/$workspaceId-ssh-config" +echo "Host $workspaceId" > "$sshConfig" +echo " Hostname \"$workspaceId.ssh.$clusterHost\"" >> "$sshConfig" +echo " User \"$workspaceId#$ownerToken\"" >> "$sshConfig" + +while true +do + # configure remote debugging + remotePort=$(ssh -F "$sshConfig" "$workspaceId" curl http://localhost:24000/debug) + if [ -n "$remotePort" ]; then + localPort=${2-$remotePort} + # forward + echo "Forwarding Debug Port: $localPort -> $remotePort" + ssh -F "$sshConfig" -L "$remotePort:localhost:$localPort" "$workspaceId" -N + fi + + sleep 1 +done diff --git a/components/ide/jetbrains/image/status/main.go b/components/ide/jetbrains/image/status/main.go index f30e59e777b9bd..d134a31a464096 100644 --- a/components/ide/jetbrains/image/status/main.go +++ b/components/ide/jetbrains/image/status/main.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "net/url" "os" @@ -20,6 +21,7 @@ import ( "path/filepath" "reflect" "regexp" + "strconv" "strings" "syscall" "time" @@ -91,7 +93,14 @@ func main() { } } - err = configureVMOptions(gitpodConfig, alias) + idePrefix := alias + if alias == "intellij" { + idePrefix = "idea" + } + // [idea64|goland64|pycharm64|phpstorm64].vmoptions + vmOptionsPath := fmt.Sprintf("/ide-desktop/backend/bin/%s64.vmoptions", idePrefix) + + err = configureVMOptions(gitpodConfig, alias, vmOptionsPath) if err != nil { log.WithError(err).Error("failed to configure vmoptions") } @@ -101,22 +110,56 @@ func main() { } go run(wsInfo, alias) - http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) { - err := terminateIDE(defaultBackendPort) + debugAgentPrefix := "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + http.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) { + options, err := readVMOptions(vmOptionsPath) if err != nil { - log.WithError(err).Error("failed to terminate IDE") - - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(err.Error())) - - os.Exit(1) + log.WithError(err).Error("failed to configure debug agent") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + debugPort := "" + i := len(options) - 1 + for i >= 0 && debugPort == "" { + option := options[i] + if strings.HasPrefix(option, debugAgentPrefix) { + debugPort = option[len(debugAgentPrefix):] + if debugPort == "0" { + debugPort = "" + } + } + i-- } - log.Info("asked IDE to terminate") - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("ok")) - os.Exit(0) + if debugPort != "" { + fmt.Fprint(w, debugPort) + return + } + netListener, err := net.Listen("tcp", "localhost:0") + if err != nil { + log.WithError(err).Error("failed to configure debug agent") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + debugPort = strconv.Itoa(netListener.(*net.TCPListener).Addr().(*net.TCPAddr).Port) + _ = netListener.Close() + + debugOptions := []string{debugAgentPrefix + debugPort} + options = deduplicateVMOption(options, debugOptions, func(l, r string) bool { + return strings.HasPrefix(l, debugAgentPrefix) && strings.HasPrefix(r, debugAgentPrefix) + }) + err = writeVMOptions(vmOptionsPath, options) + if err != nil { + log.WithError(err).Error("failed to configure debug agent") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprint(w, debugPort) + restart(r) + }) + http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "terminated") + restart(r) }) http.HandleFunc("/joinLink", func(w http.ResponseWriter, r *http.Request) { backendPort := r.URL.Query().Get("backendPort") @@ -170,6 +213,19 @@ func main() { } } +func restart(r *http.Request) { + backendPort := r.URL.Query().Get("backendPort") + if backendPort == "" { + backendPort = defaultBackendPort + } + err := terminateIDE(backendPort) + if err != nil { + log.WithError(err).Error("failed to terminate IDE gracefully") + os.Exit(1) + } + os.Exit(0) +} + type Projects struct { JoinLink string `json:"joinLink"` } @@ -324,19 +380,27 @@ func handleSignal(projectPath string) { log.Info("asked IDE to terminate") } -func configureVMOptions(config *gitpod.GitpodConfig, alias string) error { - idePrefix := alias - if alias == "intellij" { - idePrefix = "idea" - } - // [idea64|goland64|pycharm64|phpstorm64].vmoptions - path := fmt.Sprintf("/ide-desktop/backend/bin/%s64.vmoptions", idePrefix) - content, err := ioutil.ReadFile(path) +func configureVMOptions(config *gitpod.GitpodConfig, alias string, vmOptionsPath string) error { + options, err := readVMOptions(vmOptionsPath) if err != nil { return err } - newContent := updateVMOptions(config, alias, string(content)) - return ioutil.WriteFile(path, []byte(newContent), 0) + newOptions := updateVMOptions(config, alias, options) + return writeVMOptions(vmOptionsPath, newOptions) +} + +func readVMOptions(vmOptionsPath string) ([]string, error) { + content, err := ioutil.ReadFile(vmOptionsPath) + if err != nil { + return nil, err + } + return strings.Fields(string(content)), nil +} + +func writeVMOptions(vmOptionsPath string, vmoptions []string) error { + // vmoptions file should end with a newline + content := strings.Join(vmoptions, "\n") + "\n" + return ioutil.WriteFile(vmOptionsPath, []byte(content), 0) } // deduplicateVMOption append new VMOptions onto old VMOptions and remove any duplicated leftmost options @@ -357,7 +421,11 @@ func deduplicateVMOption(oldLines []string, newLines []string, predicate func(l, return result } -func updateVMOptions(config *gitpod.GitpodConfig, alias string, content string) string { +func updateVMOptions( + config *gitpod.GitpodConfig, + alias string, + // original vmoptions (inherited from $JETBRAINS_IDE_HOME/bin/idea64.vmoptions) + ideaVMOptionsLines []string) []string { // inspired by how intellij platform merge the VMOptions // https://github.com/JetBrains/intellij-community/blob/master/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java#L1115 filterFunc := func(l, r string) bool { @@ -369,8 +437,6 @@ func updateVMOptions(config *gitpod.GitpodConfig, alias string, content string) strings.Split(l, "=")[0] == strings.Split(r, "=")[0] return isEqual || isXmx || isXms || isXss || isXXOptions } - // original vmoptions (inherited from $JETBRAINS_IDE_HOME/bin/idea64.vmoptions) - ideaVMOptionsLines := strings.Fields(content) // Gitpod's default customization gitpodVMOptions := []string{"-Dgtw.disable.exit.dialog=true"} vmoptions := deduplicateVMOption(ideaVMOptionsLines, gitpodVMOptions, filterFunc) @@ -393,8 +459,7 @@ func updateVMOptions(config *gitpod.GitpodConfig, alias string, content string) } } - // vmoptions file should end with a newline - return strings.Join(vmoptions, "\n") + "\n" + return vmoptions } /* diff --git a/components/ide/jetbrains/image/status/main_test.go b/components/ide/jetbrains/image/status/main_test.go index f12f8d57216ec5..7b090beeea0618 100644 --- a/components/ide/jetbrains/image/status/main_test.go +++ b/components/ide/jetbrains/image/status/main_test.go @@ -58,17 +58,17 @@ func TestUpdateVMOptions(t *testing.T) { lessFunc := func(a, b string) bool { return a < b } t.Run(test.Desc, func(t *testing.T) { - actual := updateVMOptions(nil, test.Alias, test.Src) - if diff := cmp.Diff(strings.Fields(test.Expectation), strings.Fields(actual), cmpopts.SortSlices(lessFunc)); diff != "" { + actual := updateVMOptions(nil, test.Alias, strings.Fields(test.Src)) + if diff := cmp.Diff(strings.Fields(test.Expectation), actual, cmpopts.SortSlices(lessFunc)); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }) t.Run("updateVMOptions multiple time should be stable", func(t *testing.T) { - actual := test.Src + actual := strings.Fields(test.Src) for i := 0; i < 5; i++ { actual = updateVMOptions(nil, test.Alias, actual) - if diff := cmp.Diff(strings.Fields(test.Expectation), strings.Fields(actual), cmpopts.SortSlices(lessFunc)); diff != "" { + if diff := cmp.Diff(strings.Fields(test.Expectation), actual, cmpopts.SortSlices(lessFunc)); diff != "" { t.Errorf("unexpected output (-want +got):\n%s", diff) } }