Skip to content

Commit 05fc8f5

Browse files
committed
fix #13815: jetbrains: launch with interactive login shell in async manner
1 parent 64ed64a commit 05fc8f5

11 files changed

+169
-155
lines changed

components/ide/jetbrains/image/leeway.Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ ARG SUPERVISOR_IDE_CONFIG
1414
# ensures right permissions for /ide-desktop
1515
COPY --from=base_builder --chown=33333:33333 /ide-desktop/ /ide-desktop/
1616
COPY --chown=33333:33333 ${SUPERVISOR_IDE_CONFIG} /ide-desktop/supervisor-ide-config.json
17-
COPY --chown=33333:33333 startup.sh /ide-desktop/
1817
COPY --chown=33333:33333 components-ide-jetbrains-image--download-${JETBRAINS_DOWNLOAD_QUALIFIER}/backend /ide-desktop/backend
1918
COPY --chown=33333:33333 components-ide-jetbrains-image-status--app/status /ide-desktop
2019

components/ide/jetbrains/image/startup.sh

Lines changed: 0 additions & 21 deletions
This file was deleted.

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

Lines changed: 161 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -49,54 +49,23 @@ const BackendPath = "/ide-desktop/backend"
4949
const ProductInfoPath = BackendPath + "/product-info.json"
5050

5151
type LaunchContext struct {
52-
alias string
52+
startTime time.Time
53+
54+
port string
55+
alias string
56+
label string
57+
58+
info *ProductInfo
59+
backendVersion *version.Version
60+
wsInfo *supervisor.WorkspaceInfoResponse
61+
62+
vmOptionsFile string
5363
projectDir string
5464
configDir string
5565
systemDir string
5666
projectConfigDir string
5767
projectContextDir string
5868
riderSolutionFile string
59-
wsInfo *supervisor.WorkspaceInfoResponse
60-
}
61-
62-
// TODO(andreafalzetti): remove dir scanning once this is implemented https://youtrack.jetbrains.com/issue/GTW-2402/Rider-Open-Project-dialog-not-displaying-in-remote-dev
63-
func findRiderSolutionFile(root string) (string, error) {
64-
slnRegEx := regexp.MustCompile(`^.+\.sln$`)
65-
projRegEx := regexp.MustCompile(`^.+\.csproj$`)
66-
67-
var slnFiles []string
68-
var csprojFiles []string
69-
70-
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
71-
if err != nil {
72-
return err
73-
} else if slnRegEx.MatchString(info.Name()) {
74-
slnFiles = append(slnFiles, path)
75-
} else if projRegEx.MatchString(info.Name()) {
76-
csprojFiles = append(csprojFiles, path)
77-
}
78-
return nil
79-
})
80-
81-
if err != nil {
82-
return "", err
83-
}
84-
85-
if len(slnFiles) > 0 {
86-
return slnFiles[0], nil
87-
} else if len(csprojFiles) > 0 {
88-
return csprojFiles[0], nil
89-
}
90-
91-
return root, nil
92-
}
93-
94-
func resolveProjectContextDir(launchCtx *LaunchContext) string {
95-
if launchCtx.alias == "rider" {
96-
return launchCtx.riderSolutionFile
97-
}
98-
99-
return launchCtx.projectDir
10069
}
10170

10271
// JB startup entrypoint
@@ -133,81 +102,28 @@ func main() {
133102
return
134103
}
135104

136-
projectDir := wsInfo.GetCheckoutLocation()
137-
gitpodConfig, err := parseGitpodConfig(projectDir)
138-
if err != nil {
139-
log.WithError(err).Error("failed to parse .gitpod.yml")
140-
}
141-
142-
// configure vmoptions
143-
idePrefix := alias
144-
if alias == "intellij" {
145-
idePrefix = "idea"
146-
}
147-
// [idea64|goland64|pycharm64|phpstorm64].vmoptions
148-
vmOptionsPath := fmt.Sprintf("/ide-desktop/backend/bin/%s64.vmoptions", idePrefix)
149-
err = configureVMOptions(gitpodConfig, alias, vmOptionsPath)
150-
if err != nil {
151-
log.WithError(err).Error("failed to configure vmoptions")
152-
}
153-
154-
qualifier := os.Getenv("JETBRAINS_BACKEND_QUALIFIER")
155-
if qualifier == "stable" {
156-
qualifier = ""
157-
} else {
158-
qualifier = "-" + qualifier
159-
}
160-
161-
var riderSolutionFile string
162-
if alias == "rider" {
163-
riderSolutionFile, err = findRiderSolutionFile(projectDir)
164-
if err != nil {
165-
log.WithError(err).Error("failed to find a rider solution file")
166-
}
167-
}
168-
169-
configDir := fmt.Sprintf("/workspace/.config/JetBrains%s", qualifier)
170105
launchCtx := &LaunchContext{
171-
alias: alias,
172-
wsInfo: wsInfo,
173-
projectDir: projectDir,
174-
projectContextDir: projectDir,
175-
configDir: configDir,
176-
systemDir: fmt.Sprintf("/workspace/.cache/JetBrains%s", qualifier),
177-
riderSolutionFile: riderSolutionFile,
178-
}
106+
startTime: startTime,
179107

180-
launchCtx.projectContextDir = resolveProjectContextDir(launchCtx)
181-
launchCtx.projectConfigDir = fmt.Sprintf("%s/RemoteDev-%s/%s", configDir, info.ProductCode, strings.ReplaceAll(launchCtx.projectContextDir, "/", "_"))
108+
port: port,
109+
alias: alias,
110+
label: label,
182111

183-
// sync initial options
184-
err = syncOptions(launchCtx)
185-
if err != nil {
186-
log.WithError(err).Error("failed to sync initial options")
112+
info: info,
113+
backendVersion: backendVersion,
114+
wsInfo: wsInfo,
187115
}
116+
// we should start serving immediately and postpone launch
117+
// in order to enable a JB Gateway to connect as soon as possible
118+
go launch(launchCtx)
119+
// IMPORTANT: don't put startup logic in serve!!!
120+
serve(launchCtx)
121+
}
188122

189-
// install project plugins
190-
version_2022_1, _ := version.NewVersion("2022.1")
191-
if version_2022_1.LessThanOrEqual(backendVersion) {
192-
err = installPlugins(gitpodConfig, launchCtx)
193-
installPluginsCost := time.Now().Local().Sub(startTime).Milliseconds()
194-
if err != nil {
195-
log.WithError(err).WithField("cost", installPluginsCost).Error("installing repo plugins: done")
196-
} else {
197-
log.WithField("cost", installPluginsCost).Info("installing repo plugins: done")
198-
}
199-
}
200-
201-
// install gitpod plugin
202-
err = linkRemotePlugin()
203-
if err != nil {
204-
log.WithError(err).Error("failed to install gitpod-remote plugin")
205-
}
206-
go run(launchCtx)
207-
123+
func serve(launchCtx *LaunchContext) {
208124
debugAgentPrefix := "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:"
209125
http.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) {
210-
options, err := readVMOptions(vmOptionsPath)
126+
options, err := readVMOptions(launchCtx.vmOptionsFile)
211127
if err != nil {
212128
log.WithError(err).Error("failed to configure debug agent")
213129
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -243,7 +159,7 @@ func main() {
243159
options = deduplicateVMOption(options, debugOptions, func(l, r string) bool {
244160
return strings.HasPrefix(l, debugAgentPrefix) && strings.HasPrefix(r, debugAgentPrefix)
245161
})
246-
err = writeVMOptions(vmOptionsPath, options)
162+
err = writeVMOptions(launchCtx.vmOptionsFile, options)
247163
if err != nil {
248164
log.WithError(err).Error("failed to configure debug agent")
249165
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -274,7 +190,7 @@ func main() {
274190
if backendPort == "" {
275191
backendPort = defaultBackendPort
276192
}
277-
jsonLink, err := resolveGatewayLink(backendPort, wsInfo)
193+
jsonLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo)
278194
if err != nil {
279195
log.WithError(err).Error("cannot resolve gateway link")
280196
http.Error(w, err.Error(), http.StatusServiceUnavailable)
@@ -287,23 +203,23 @@ func main() {
287203
if backendPort == "" {
288204
backendPort = defaultBackendPort
289205
}
290-
gatewayLink, err := resolveGatewayLink(backendPort, wsInfo)
206+
gatewayLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo)
291207
if err != nil {
292208
log.WithError(err).Error("cannot resolve gateway link")
293209
http.Error(w, err.Error(), http.StatusServiceUnavailable)
294210
return
295211
}
296212
response := make(map[string]string)
297213
response["link"] = gatewayLink
298-
response["label"] = label
214+
response["label"] = launchCtx.label
299215
response["clientID"] = "jetbrains-gateway"
300-
response["kind"] = alias
216+
response["kind"] = launchCtx.alias
301217
w.Header().Set("Content-Type", "application/json")
302218
_ = json.NewEncoder(w).Encode(response)
303219
})
304220

305-
fmt.Printf("Starting status proxy for desktop IDE at port %s\n", port)
306-
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil {
221+
fmt.Printf("Starting status proxy for desktop IDE at port %s\n", launchCtx.port)
222+
if err := http.ListenAndServe(fmt.Sprintf(":%s", launchCtx.port), nil); err != nil {
307223
log.Fatal(err)
308224
}
309225
}
@@ -415,6 +331,76 @@ func resolveWorkspaceInfo(ctx context.Context) (*supervisor.WorkspaceInfoRespons
415331
return nil, errors.New("failed with attempt 10 times")
416332
}
417333

334+
func launch(launchCtx *LaunchContext) {
335+
projectDir := launchCtx.wsInfo.GetCheckoutLocation()
336+
gitpodConfig, err := parseGitpodConfig(projectDir)
337+
if err != nil {
338+
log.WithError(err).Error("failed to parse .gitpod.yml")
339+
}
340+
341+
// configure vmoptions
342+
idePrefix := launchCtx.alias
343+
if launchCtx.alias == "intellij" {
344+
idePrefix = "idea"
345+
}
346+
// [idea64|goland64|pycharm64|phpstorm64].vmoptions
347+
launchCtx.vmOptionsFile = fmt.Sprintf("/ide-desktop/backend/bin/%s64.vmoptions", idePrefix)
348+
err = configureVMOptions(gitpodConfig, launchCtx.alias, launchCtx.vmOptionsFile)
349+
if err != nil {
350+
log.WithError(err).Error("failed to configure vmoptions")
351+
}
352+
353+
qualifier := os.Getenv("JETBRAINS_BACKEND_QUALIFIER")
354+
if qualifier == "stable" {
355+
qualifier = ""
356+
} else {
357+
qualifier = "-" + qualifier
358+
}
359+
360+
var riderSolutionFile string
361+
if launchCtx.alias == "rider" {
362+
riderSolutionFile, err = findRiderSolutionFile(projectDir)
363+
if err != nil {
364+
log.WithError(err).Error("failed to find a rider solution file")
365+
}
366+
}
367+
368+
configDir := fmt.Sprintf("/workspace/.config/JetBrains%s", qualifier)
369+
launchCtx.projectDir = projectDir
370+
launchCtx.configDir = configDir
371+
launchCtx.systemDir = fmt.Sprintf("/workspace/.cache/JetBrains%s", qualifier)
372+
launchCtx.riderSolutionFile = riderSolutionFile
373+
launchCtx.projectContextDir = resolveProjectContextDir(launchCtx)
374+
launchCtx.projectConfigDir = fmt.Sprintf("%s/RemoteDev-%s/%s", configDir, launchCtx.info.ProductCode, strings.ReplaceAll(launchCtx.projectContextDir, "/", "_"))
375+
376+
// sync initial options
377+
err = syncOptions(launchCtx)
378+
if err != nil {
379+
log.WithError(err).Error("failed to sync initial options")
380+
}
381+
382+
// install project plugins
383+
version_2022_1, _ := version.NewVersion("2022.1")
384+
if version_2022_1.LessThanOrEqual(launchCtx.backendVersion) {
385+
err = installPlugins(gitpodConfig, launchCtx)
386+
installPluginsCost := time.Now().Local().Sub(launchCtx.startTime).Milliseconds()
387+
if err != nil {
388+
log.WithError(err).WithField("cost", installPluginsCost).Error("installing repo plugins: done")
389+
} else {
390+
log.WithField("cost", installPluginsCost).Info("installing repo plugins: done")
391+
}
392+
}
393+
394+
// install gitpod plugin
395+
err = linkRemotePlugin()
396+
if err != nil {
397+
log.WithError(err).Error("failed to install gitpod-remote plugin")
398+
}
399+
400+
// run backend
401+
run(launchCtx)
402+
}
403+
418404
func run(launchCtx *LaunchContext) {
419405
var args []string
420406
args = append(args, "run")
@@ -443,14 +429,24 @@ func run(launchCtx *LaunchContext) {
443429
os.Exit(cmd.ProcessState.ExitCode())
444430
}
445431

446-
func remoteDevServerCmd(args []string, productContext *LaunchContext) *exec.Cmd {
447-
cmd := exec.Command(BackendPath+"/bin/remote-dev-server.sh", args...)
448-
cmd.Env = os.Environ()
449-
450-
// Set default config and system directories under /workspace to preserve between restarts
451-
cmd.Env = append(cmd.Env,
452-
fmt.Sprintf("IJ_HOST_CONFIG_BASE_DIR=%s", productContext.configDir),
453-
fmt.Sprintf("IJ_HOST_SYSTEM_BASE_DIR=%s", productContext.systemDir),
432+
func remoteDevServerCmd(args []string, launchCtx *LaunchContext) *exec.Cmd {
433+
shell := os.Getenv("SHELL")
434+
if shell == "" {
435+
shell = "/bin/bash"
436+
}
437+
// Emulate the interactive login shell to ensure that all users defined shell scripts are loaded
438+
shellArgs := []string{"-ilc", BackendPath + "/bin/remote-dev-server.sh"}
439+
shellArgs = append(shellArgs, args...)
440+
cmd := exec.Command(shell, shellArgs...)
441+
442+
cmd.Env = append(os.Environ(),
443+
// Set default config and system directories under /workspace to preserve between restarts
444+
fmt.Sprintf("IJ_HOST_CONFIG_BASE_DIR=%s", launchCtx.configDir),
445+
fmt.Sprintf("IJ_HOST_SYSTEM_BASE_DIR=%s", launchCtx.systemDir),
446+
// instead put them into /ide-desktop/backend/bin/idea64.vmoptions
447+
// otherwise JB will complain to a user on each startup
448+
// by default remote dev already set -Xmx2048m, see /ide-desktop/backend/plugins/remote-dev-server/bin/launcher.sh
449+
"JAVA_TOOL_OPTIONS=",
454450
)
455451

456452
cmd.Stderr = os.Stderr
@@ -731,3 +727,43 @@ func linkRemotePlugin() error {
731727
}
732728
return os.Symlink("/ide-desktop-plugins/gitpod-remote", remotePluginDir)
733729
}
730+
731+
// TODO(andreafalzetti): remove dir scanning once this is implemented https://youtrack.jetbrains.com/issue/GTW-2402/Rider-Open-Project-dialog-not-displaying-in-remote-dev
732+
func findRiderSolutionFile(root string) (string, error) {
733+
slnRegEx := regexp.MustCompile(`^.+\.sln$`)
734+
projRegEx := regexp.MustCompile(`^.+\.csproj$`)
735+
736+
var slnFiles []string
737+
var csprojFiles []string
738+
739+
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
740+
if err != nil {
741+
return err
742+
} else if slnRegEx.MatchString(info.Name()) {
743+
slnFiles = append(slnFiles, path)
744+
} else if projRegEx.MatchString(info.Name()) {
745+
csprojFiles = append(csprojFiles, path)
746+
}
747+
return nil
748+
})
749+
750+
if err != nil {
751+
return "", err
752+
}
753+
754+
if len(slnFiles) > 0 {
755+
return slnFiles[0], nil
756+
} else if len(csprojFiles) > 0 {
757+
return csprojFiles[0], nil
758+
}
759+
760+
return root, nil
761+
}
762+
763+
func resolveProjectContextDir(launchCtx *LaunchContext) string {
764+
if launchCtx.alias == "rider" {
765+
return launchCtx.riderSolutionFile
766+
}
767+
768+
return launchCtx.projectDir
769+
}

0 commit comments

Comments
 (0)