Skip to content

Commit

Permalink
jb: allow backend debugging running in preview env
Browse files Browse the repository at this point in the history
  • Loading branch information
akosyakov authored and roboquat committed Sep 12, 2022
1 parent d88a819 commit fcb426e
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 34 deletions.
10 changes: 9 additions & 1 deletion components/ide/jetbrains/backend-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img src="https://user-images.githubusercontent.com/3082655/187091748-c58ce156-90b6-4522-83a7-b4312e36d949.png"/>

### 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:
Expand Down Expand Up @@ -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 <workspaceURL>` 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.
Expand All @@ -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=<workspaceURL>
```

### Remote debugging

Run `./remote-debug.sh <workspaceURL> (<localPort>)?` 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.
48 changes: 48 additions & 0 deletions components/ide/jetbrains/backend-plugin/remote-debug.sh
Original file line number Diff line number Diff line change
@@ -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> (<localPort>)?

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
123 changes: 94 additions & 29 deletions components/ide/jetbrains/image/status/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
Expand All @@ -20,6 +21,7 @@ import (
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -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")
}
Expand All @@ -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")
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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
}

/*
Expand Down
8 changes: 4 additions & 4 deletions components/ide/jetbrains/image/status/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down

0 comments on commit fcb426e

Please sign in to comment.