Skip to content

Commit 358b5a5

Browse files
committed
jb: allow backend debugging running in preview env
1 parent a809067 commit 358b5a5

File tree

4 files changed

+155
-34
lines changed

4 files changed

+155
-34
lines changed

components/ide/jetbrains/backend-plugin/README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ IntelliJ delivers better experience for development of JetBrains plugins. We sho
1212
issues [here](https://youtrack.jetbrains.com/issues?q=project:%20CWM)
1313
under remote development subsystem.
1414

15+
<img src="https://user-images.githubusercontent.com/3082655/187091748-c58ce156-90b6-4522-83a7-b4312e36d949.png"/>
16+
1517
### Local
1618

1719
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
5052
update the IDE config map in a preview environment. After that start a new workspace in preview environment
5153
with corresponding version to try your changes.
5254

53-
### Hot swap
55+
### Hot swapping
5456

5557
Run `./hot-swap.sh <workspaceURL>` to build a new backend plugin version corresponding to a workspace running in preview environment,
5658
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:
5961
```bash
6062
leeway build components/ide/jetbrains/image/status:hot-swap -DworkspaceUrl=<workspaceURL>
6163
```
64+
65+
### Remote debugging
66+
67+
Run `./remote-debug.sh <workspaceURL> (<localPort>)?` to configure remote debugging in a workpace running in preview environment.
68+
It will configure remote debug port, restart the backend and start port forwarding in your dev workspace.
69+
Create a new `Remote JVM Debug` launch configuration with the forwarded port and launch it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
# Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
# Licensed under the GNU Affero General Public License (AGPL).
4+
# See License-AGPL.txt in the project root for license information.
5+
6+
# This script configure remote debugging in a workspace running in a preview environment.
7+
# It updates VM options with remote debug agent, restart the JB backend to apply them,
8+
# and start port forwarding of the remote debug port. You can configure `Remote JVM Debug`
9+
# run configuration using the forwarded port.
10+
#
11+
# ./remote-debug.sh <workspaceUrl> (<localPort>)?
12+
13+
workspaceUrl=${1-}
14+
[ -z "$workspaceUrl" ] && echo "Please provide a workspace URL as first argument." && exit 1
15+
workspaceUrl=$(echo "$workspaceUrl" |sed -e "s/\/$//")
16+
echo "URL: $workspaceUrl"
17+
18+
workspaceDesc=$(gpctl workspaces describe "$workspaceUrl" -o=json)
19+
20+
podName=$(echo "$workspaceDesc" | jq .runtime.pod_name -r)
21+
echo "Pod: $podName"
22+
23+
workspaceId=$(echo "$workspaceDesc" | jq .metadata.meta_id -r)
24+
echo "ID: $workspaceId"
25+
26+
clusterHost=$(kubectl exec -it "$podName" -- printenv GITPOD_WORKSPACE_CLUSTER_HOST |sed -e "s/\s//g")
27+
echo "Cluster Host: $clusterHost"
28+
29+
# prepare ssh
30+
ownerToken=$(kubectl get pod "$podName" -o=json | jq ".metadata.annotations.\"gitpod\/ownerToken\"" -r)
31+
sshConfig="/tmp/$workspaceId-ssh-config"
32+
echo "Host $workspaceId" > "$sshConfig"
33+
echo " Hostname \"$workspaceId.ssh.$clusterHost\"" >> "$sshConfig"
34+
echo " User \"$workspaceId#$ownerToken\"" >> "$sshConfig"
35+
36+
while true
37+
do
38+
# configure remote debugging
39+
remotePort=$(ssh -F "$sshConfig" "$workspaceId" curl http://localhost:24000/debug)
40+
if [ -n "$remotePort" ]; then
41+
localPort=${2-$remotePort}
42+
# forward
43+
echo "Forwarding Debug Port: $localPort -> $remotePort"
44+
ssh -F "$sshConfig" -L "$remotePort:localhost:$localPort" "$workspaceId" -N
45+
fi
46+
47+
sleep 1
48+
done

components/ide/jetbrains/image/status/main.go

+94-29
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"io"
1414
"io/ioutil"
15+
"net"
1516
"net/http"
1617
"net/url"
1718
"os"
@@ -20,6 +21,7 @@ import (
2021
"path/filepath"
2122
"reflect"
2223
"regexp"
24+
"strconv"
2325
"strings"
2426
"syscall"
2527
"time"
@@ -91,7 +93,14 @@ func main() {
9193
}
9294
}
9395

94-
err = configureVMOptions(gitpodConfig, alias)
96+
idePrefix := alias
97+
if alias == "intellij" {
98+
idePrefix = "idea"
99+
}
100+
// [idea64|goland64|pycharm64|phpstorm64].vmoptions
101+
vmOptionsPath := fmt.Sprintf("/ide-desktop/backend/bin/%s64.vmoptions", idePrefix)
102+
103+
err = configureVMOptions(gitpodConfig, alias, vmOptionsPath)
95104
if err != nil {
96105
log.WithError(err).Error("failed to configure vmoptions")
97106
}
@@ -101,22 +110,56 @@ func main() {
101110
}
102111
go run(wsInfo, alias)
103112

104-
http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) {
105-
err := terminateIDE(defaultBackendPort)
113+
debugAgentPrefix := "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:"
114+
http.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) {
115+
options, err := readVMOptions(vmOptionsPath)
106116
if err != nil {
107-
log.WithError(err).Error("failed to terminate IDE")
108-
109-
w.WriteHeader(http.StatusInternalServerError)
110-
_, _ = w.Write([]byte(err.Error()))
111-
112-
os.Exit(1)
117+
log.WithError(err).Error("failed to configure debug agent")
118+
http.Error(w, err.Error(), http.StatusInternalServerError)
119+
return
120+
}
121+
debugPort := ""
122+
i := len(options) - 1
123+
for i >= 0 && debugPort == "" {
124+
option := options[i]
125+
if strings.HasPrefix(option, debugAgentPrefix) {
126+
debugPort = option[len(debugAgentPrefix):]
127+
if debugPort == "0" {
128+
debugPort = ""
129+
}
130+
}
131+
i--
113132
}
114-
log.Info("asked IDE to terminate")
115-
116-
w.WriteHeader(http.StatusOK)
117-
_, _ = w.Write([]byte("ok"))
118133

119-
os.Exit(0)
134+
if debugPort != "" {
135+
fmt.Fprint(w, debugPort)
136+
return
137+
}
138+
netListener, err := net.Listen("tcp", "localhost:0")
139+
if err != nil {
140+
log.WithError(err).Error("failed to configure debug agent")
141+
http.Error(w, err.Error(), http.StatusInternalServerError)
142+
return
143+
}
144+
debugPort = strconv.Itoa(netListener.(*net.TCPListener).Addr().(*net.TCPAddr).Port)
145+
_ = netListener.Close()
146+
147+
debugOptions := []string{debugAgentPrefix + debugPort}
148+
options = deduplicateVMOption(options, debugOptions, func(l, r string) bool {
149+
return strings.HasPrefix(l, debugAgentPrefix) && strings.HasPrefix(r, debugAgentPrefix)
150+
})
151+
err = writeVMOptions(vmOptionsPath, options)
152+
if err != nil {
153+
log.WithError(err).Error("failed to configure debug agent")
154+
http.Error(w, err.Error(), http.StatusInternalServerError)
155+
return
156+
}
157+
fmt.Fprint(w, debugPort)
158+
restart(r)
159+
})
160+
http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) {
161+
fmt.Fprint(w, "terminated")
162+
restart(r)
120163
})
121164
http.HandleFunc("/joinLink", func(w http.ResponseWriter, r *http.Request) {
122165
backendPort := r.URL.Query().Get("backendPort")
@@ -170,6 +213,19 @@ func main() {
170213
}
171214
}
172215

216+
func restart(r *http.Request) {
217+
backendPort := r.URL.Query().Get("backendPort")
218+
if backendPort == "" {
219+
backendPort = defaultBackendPort
220+
}
221+
err := terminateIDE(backendPort)
222+
if err != nil {
223+
log.WithError(err).Error("failed to terminate IDE gracefully")
224+
os.Exit(1)
225+
}
226+
os.Exit(0)
227+
}
228+
173229
type Projects struct {
174230
JoinLink string `json:"joinLink"`
175231
}
@@ -324,19 +380,27 @@ func handleSignal(projectPath string) {
324380
log.Info("asked IDE to terminate")
325381
}
326382

327-
func configureVMOptions(config *gitpod.GitpodConfig, alias string) error {
328-
idePrefix := alias
329-
if alias == "intellij" {
330-
idePrefix = "idea"
331-
}
332-
// [idea64|goland64|pycharm64|phpstorm64].vmoptions
333-
path := fmt.Sprintf("/ide-desktop/backend/bin/%s64.vmoptions", idePrefix)
334-
content, err := ioutil.ReadFile(path)
383+
func configureVMOptions(config *gitpod.GitpodConfig, alias string, vmOptionsPath string) error {
384+
options, err := readVMOptions(vmOptionsPath)
335385
if err != nil {
336386
return err
337387
}
338-
newContent := updateVMOptions(config, alias, string(content))
339-
return ioutil.WriteFile(path, []byte(newContent), 0)
388+
newOptions := updateVMOptions(config, alias, options)
389+
return writeVMOptions(vmOptionsPath, newOptions)
390+
}
391+
392+
func readVMOptions(vmOptionsPath string) ([]string, error) {
393+
content, err := ioutil.ReadFile(vmOptionsPath)
394+
if err != nil {
395+
return nil, err
396+
}
397+
return strings.Fields(string(content)), nil
398+
}
399+
400+
func writeVMOptions(vmOptionsPath string, vmoptions []string) error {
401+
// vmoptions file should end with a newline
402+
content := strings.Join(vmoptions, "\n") + "\n"
403+
return ioutil.WriteFile(vmOptionsPath, []byte(content), 0)
340404
}
341405

342406
// 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,
357421
return result
358422
}
359423

360-
func updateVMOptions(config *gitpod.GitpodConfig, alias string, content string) string {
424+
func updateVMOptions(
425+
config *gitpod.GitpodConfig,
426+
alias string,
427+
// original vmoptions (inherited from $JETBRAINS_IDE_HOME/bin/idea64.vmoptions)
428+
ideaVMOptionsLines []string) []string {
361429
// inspired by how intellij platform merge the VMOptions
362430
// https://github.com/JetBrains/intellij-community/blob/master/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java#L1115
363431
filterFunc := func(l, r string) bool {
@@ -369,8 +437,6 @@ func updateVMOptions(config *gitpod.GitpodConfig, alias string, content string)
369437
strings.Split(l, "=")[0] == strings.Split(r, "=")[0]
370438
return isEqual || isXmx || isXms || isXss || isXXOptions
371439
}
372-
// original vmoptions (inherited from $JETBRAINS_IDE_HOME/bin/idea64.vmoptions)
373-
ideaVMOptionsLines := strings.Fields(content)
374440
// Gitpod's default customization
375441
gitpodVMOptions := []string{"-Dgtw.disable.exit.dialog=true"}
376442
vmoptions := deduplicateVMOption(ideaVMOptionsLines, gitpodVMOptions, filterFunc)
@@ -393,8 +459,7 @@ func updateVMOptions(config *gitpod.GitpodConfig, alias string, content string)
393459
}
394460
}
395461

396-
// vmoptions file should end with a newline
397-
return strings.Join(vmoptions, "\n") + "\n"
462+
return vmoptions
398463
}
399464

400465
/*

components/ide/jetbrains/image/status/main_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,17 @@ func TestUpdateVMOptions(t *testing.T) {
5858
lessFunc := func(a, b string) bool { return a < b }
5959

6060
t.Run(test.Desc, func(t *testing.T) {
61-
actual := updateVMOptions(nil, test.Alias, test.Src)
62-
if diff := cmp.Diff(strings.Fields(test.Expectation), strings.Fields(actual), cmpopts.SortSlices(lessFunc)); diff != "" {
61+
actual := updateVMOptions(nil, test.Alias, strings.Fields(test.Src))
62+
if diff := cmp.Diff(strings.Fields(test.Expectation), actual, cmpopts.SortSlices(lessFunc)); diff != "" {
6363
t.Errorf("unexpected output (-want +got):\n%s", diff)
6464
}
6565
})
6666

6767
t.Run("updateVMOptions multiple time should be stable", func(t *testing.T) {
68-
actual := test.Src
68+
actual := strings.Fields(test.Src)
6969
for i := 0; i < 5; i++ {
7070
actual = updateVMOptions(nil, test.Alias, actual)
71-
if diff := cmp.Diff(strings.Fields(test.Expectation), strings.Fields(actual), cmpopts.SortSlices(lessFunc)); diff != "" {
71+
if diff := cmp.Diff(strings.Fields(test.Expectation), actual, cmpopts.SortSlices(lessFunc)); diff != "" {
7272
t.Errorf("unexpected output (-want +got):\n%s", diff)
7373
}
7474
}

0 commit comments

Comments
 (0)