Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ide/jetbrains/launcher/main.go
2501 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package main
6
7
import (
8
"archive/zip"
9
"bytes"
10
"context"
11
"encoding/json"
12
"errors"
13
"fmt"
14
"io"
15
"io/fs"
16
"io/ioutil"
17
"net"
18
"net/http"
19
"net/url"
20
"os"
21
"os/exec"
22
"os/signal"
23
"path/filepath"
24
"reflect"
25
"regexp"
26
"strconv"
27
"strings"
28
"syscall"
29
"time"
30
31
"github.com/google/uuid"
32
"github.com/hashicorp/go-version"
33
"golang.org/x/xerrors"
34
"google.golang.org/grpc"
35
"google.golang.org/grpc/credentials/insecure"
36
yaml "gopkg.in/yaml.v2"
37
38
"github.com/gitpod-io/gitpod/common-go/log"
39
"github.com/gitpod-io/gitpod/common-go/util"
40
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
41
"github.com/gitpod-io/gitpod/jetbrains/launcher/pkg/constant"
42
supervisor "github.com/gitpod-io/gitpod/supervisor/api"
43
)
44
45
const (
46
defaultBackendPort = "63342"
47
maxDefaultXmx = 8 * 1024
48
minDefaultXmx = 2 * 1024
49
)
50
51
var (
52
// ServiceName is the name we use for tracing/logging.
53
ServiceName = "jetbrains-launcher"
54
)
55
56
type LaunchContext struct {
57
startTime time.Time
58
59
port string
60
alias string
61
label string
62
warmup bool
63
64
preferToolbox bool
65
qualifier string
66
productDir string
67
backendDir string
68
info *ProductInfo
69
backendVersion *version.Version
70
wsInfo *supervisor.WorkspaceInfoResponse
71
72
vmOptionsFile string
73
platformPropertiesFile string
74
projectDir string
75
configDir string
76
systemDir string
77
projectContextDir string
78
riderSolutionFile string
79
80
env []string
81
82
// Custom fields
83
84
// shouldWaitBackendPlugin is controlled by env GITPOD_WAIT_IDE_BACKEND
85
shouldWaitBackendPlugin bool
86
}
87
88
func (c *LaunchContext) getCommonJoinLinkResponse(appPid int, joinLink string) *JoinLinkResponse {
89
return &JoinLinkResponse{
90
AppPid: appPid,
91
JoinLink: joinLink,
92
IDEVersion: fmt.Sprintf("%s-%s", c.info.ProductCode, c.info.BuildNumber),
93
ProjectPath: c.projectContextDir,
94
}
95
}
96
97
// JB startup entrypoint
98
func main() {
99
if len(os.Args) == 3 && os.Args[1] == "env" && os.Args[2] != "" {
100
var mark = os.Args[2]
101
content, err := json.Marshal(os.Environ())
102
exitStatus := 0
103
if err != nil {
104
fmt.Fprintf(os.Stderr, "%s", err)
105
exitStatus = 1
106
}
107
fmt.Printf("%s%s%s", mark, content, mark)
108
os.Exit(exitStatus)
109
return
110
}
111
112
// supervisor refer see https://github.com/gitpod-io/gitpod/blob/main/components/supervisor/pkg/supervisor/supervisor.go#L961
113
shouldWaitBackendPlugin := os.Getenv("GITPOD_WAIT_IDE_BACKEND") == "true"
114
debugEnabled := os.Getenv("SUPERVISOR_DEBUG_ENABLE") == "true"
115
preferToolbox := os.Getenv("GITPOD_PREFER_TOOLBOX") == "true"
116
log.Init(ServiceName, constant.Version, true, debugEnabled)
117
log.Info(ServiceName + ": " + constant.Version)
118
startTime := time.Now()
119
120
log.WithField("shouldWait", shouldWaitBackendPlugin).Info("should wait backend plugin")
121
var port string
122
var warmup bool
123
124
if len(os.Args) < 2 {
125
log.Fatalf("Usage: %s (warmup|<port>)\n", os.Args[0])
126
}
127
128
if os.Args[1] == "warmup" {
129
if len(os.Args) < 3 {
130
log.Fatalf("Usage: %s %s <alias>\n", os.Args[0], os.Args[1])
131
}
132
133
warmup = true
134
} else {
135
if len(os.Args) < 3 {
136
log.Fatalf("Usage: %s <port> <kind> [<link label>]\n", os.Args[0])
137
}
138
139
port = os.Args[1]
140
}
141
142
alias := os.Args[2]
143
label := "Open JetBrains IDE"
144
if len(os.Args) > 3 {
145
label = os.Args[3]
146
}
147
148
qualifier := os.Getenv("JETBRAINS_BACKEND_QUALIFIER")
149
if qualifier == "stable" {
150
qualifier = ""
151
} else {
152
qualifier = "-" + qualifier
153
}
154
productDir := "/ide-desktop/" + alias + qualifier
155
backendDir := productDir + "/backend"
156
157
info, err := resolveProductInfo(backendDir)
158
if err != nil {
159
log.WithError(err).Error("failed to resolve product info")
160
return
161
}
162
163
backendVersion, err := version.NewVersion(info.Version)
164
if err != nil {
165
log.WithError(err).Error("failed to resolve backend version")
166
return
167
}
168
169
wsInfo, err := resolveWorkspaceInfo(context.Background())
170
if err != nil || wsInfo == nil {
171
log.WithError(err).WithField("wsInfo", wsInfo).Error("resolve workspace info failed")
172
return
173
}
174
175
launchCtx := &LaunchContext{
176
startTime: startTime,
177
178
warmup: warmup,
179
port: port,
180
alias: alias,
181
label: label,
182
183
preferToolbox: preferToolbox,
184
qualifier: qualifier,
185
productDir: productDir,
186
backendDir: backendDir,
187
info: info,
188
backendVersion: backendVersion,
189
wsInfo: wsInfo,
190
shouldWaitBackendPlugin: shouldWaitBackendPlugin,
191
}
192
193
if launchCtx.warmup {
194
launch(launchCtx)
195
return
196
}
197
198
if preferToolbox {
199
err = configureToolboxCliProperties(backendDir)
200
if err != nil {
201
log.WithError(err).Error("failed to write toolbox cli config file")
202
return
203
}
204
}
205
206
// we should start serving immediately and postpone launch
207
// in order to enable a JB Gateway to connect as soon as possible
208
go launch(launchCtx)
209
// IMPORTANT: don't put startup logic in serve!!!
210
serve(launchCtx)
211
}
212
213
func serve(launchCtx *LaunchContext) {
214
debugAgentPrefix := "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:"
215
http.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) {
216
options, err := readVMOptions(launchCtx.vmOptionsFile)
217
if err != nil {
218
log.WithError(err).Error("failed to configure debug agent")
219
http.Error(w, err.Error(), http.StatusInternalServerError)
220
return
221
}
222
debugPort := ""
223
i := len(options) - 1
224
for i >= 0 && debugPort == "" {
225
option := options[i]
226
if strings.HasPrefix(option, debugAgentPrefix) {
227
debugPort = option[len(debugAgentPrefix):]
228
if debugPort == "0" {
229
debugPort = ""
230
}
231
}
232
i--
233
}
234
235
if debugPort != "" {
236
fmt.Fprint(w, debugPort)
237
return
238
}
239
netListener, err := net.Listen("tcp", "localhost:0")
240
if err != nil {
241
log.WithError(err).Error("failed to configure debug agent")
242
http.Error(w, err.Error(), http.StatusInternalServerError)
243
return
244
}
245
debugPort = strconv.Itoa(netListener.(*net.TCPListener).Addr().(*net.TCPAddr).Port)
246
_ = netListener.Close()
247
248
debugOptions := []string{debugAgentPrefix + debugPort}
249
options = deduplicateVMOption(options, debugOptions, func(l, r string) bool {
250
return strings.HasPrefix(l, debugAgentPrefix) && strings.HasPrefix(r, debugAgentPrefix)
251
})
252
err = writeVMOptions(launchCtx.vmOptionsFile, options)
253
if err != nil {
254
log.WithError(err).Error("failed to configure debug agent")
255
http.Error(w, err.Error(), http.StatusInternalServerError)
256
return
257
}
258
fmt.Fprint(w, debugPort)
259
restart(r)
260
})
261
http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) {
262
fmt.Fprint(w, "terminated")
263
restart(r)
264
})
265
http.HandleFunc("/joinLink", func(w http.ResponseWriter, r *http.Request) {
266
backendPort := r.URL.Query().Get("backendPort")
267
if backendPort == "" {
268
backendPort = defaultBackendPort
269
}
270
jsonLink, err := resolveJsonLink(backendPort)
271
if err != nil {
272
log.WithError(err).Error("cannot resolve join link")
273
http.Error(w, err.Error(), http.StatusServiceUnavailable)
274
return
275
}
276
fmt.Fprint(w, jsonLink)
277
})
278
http.HandleFunc("/joinLink2", func(w http.ResponseWriter, r *http.Request) {
279
backendPort := r.URL.Query().Get("backendPort")
280
if backendPort == "" {
281
backendPort = defaultBackendPort
282
}
283
jsonResp, err := resolveJsonLink2(launchCtx, backendPort)
284
if err != nil {
285
log.WithError(err).Error("cannot resolve join link")
286
http.Error(w, err.Error(), http.StatusServiceUnavailable)
287
return
288
}
289
json.NewEncoder(w).Encode(jsonResp)
290
})
291
http.HandleFunc("/gatewayLink", func(w http.ResponseWriter, r *http.Request) {
292
backendPort := r.URL.Query().Get("backendPort")
293
if backendPort == "" {
294
backendPort = defaultBackendPort
295
}
296
jsonLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo)
297
if err != nil {
298
log.WithError(err).Error("cannot resolve gateway link")
299
http.Error(w, err.Error(), http.StatusServiceUnavailable)
300
return
301
}
302
fmt.Fprint(w, jsonLink)
303
})
304
http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
305
if launchCtx.preferToolbox {
306
response := make(map[string]string)
307
toolboxLink, err := resolveToolboxLink(launchCtx.wsInfo)
308
if err != nil {
309
log.WithError(err).Error("cannot resolve toolbox link")
310
http.Error(w, err.Error(), http.StatusServiceUnavailable)
311
return
312
}
313
response["link"] = toolboxLink
314
response["label"] = launchCtx.label
315
response["clientID"] = "jetbrains-toolbox"
316
response["kind"] = launchCtx.alias
317
w.Header().Set("Content-Type", "application/json")
318
_ = json.NewEncoder(w).Encode(response)
319
return
320
}
321
backendPort := r.URL.Query().Get("backendPort")
322
if backendPort == "" {
323
backendPort = defaultBackendPort
324
}
325
if err := isBackendPluginReady(r.Context(), backendPort, launchCtx.shouldWaitBackendPlugin); err != nil {
326
http.Error(w, err.Error(), http.StatusServiceUnavailable)
327
return
328
}
329
gatewayLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo)
330
if err != nil {
331
log.WithError(err).Error("cannot resolve gateway link")
332
http.Error(w, err.Error(), http.StatusServiceUnavailable)
333
return
334
}
335
response := make(map[string]string)
336
response["link"] = gatewayLink
337
response["label"] = launchCtx.label
338
response["clientID"] = "jetbrains-gateway"
339
response["kind"] = launchCtx.alias
340
w.Header().Set("Content-Type", "application/json")
341
_ = json.NewEncoder(w).Encode(response)
342
})
343
344
fmt.Printf("Starting status proxy for desktop IDE at port %s\n", launchCtx.port)
345
if err := http.ListenAndServe(fmt.Sprintf(":%s", launchCtx.port), nil); err != nil {
346
log.Fatal(err)
347
}
348
}
349
350
// isBackendPluginReady checks if the backend plugin is ready via backend plugin CLI GitpodCLIService.kt
351
func isBackendPluginReady(ctx context.Context, backendPort string, shouldWaitBackendPlugin bool) error {
352
if !shouldWaitBackendPlugin {
353
log.Debug("will not wait plugin ready")
354
return nil
355
}
356
log.WithField("backendPort", backendPort).Debug("wait backend plugin to be ready")
357
// Use op=metrics so that we don't need to rebuild old backend-plugin
358
url, err := url.Parse("http://localhost:" + backendPort + "/api/gitpod/cli?op=metrics")
359
if err != nil {
360
return err
361
}
362
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
363
if err != nil {
364
return err
365
}
366
resp, err := http.DefaultClient.Do(req)
367
if err != nil {
368
return err
369
}
370
defer resp.Body.Close()
371
if resp.StatusCode != http.StatusOK {
372
return fmt.Errorf("backend plugin is not ready: %d", resp.StatusCode)
373
}
374
return nil
375
}
376
377
func restart(r *http.Request) {
378
backendPort := r.URL.Query().Get("backendPort")
379
if backendPort == "" {
380
backendPort = defaultBackendPort
381
}
382
err := terminateIDE(backendPort)
383
if err != nil {
384
log.WithError(err).Error("failed to terminate IDE gracefully")
385
os.Exit(1)
386
}
387
os.Exit(0)
388
}
389
390
type Projects struct {
391
JoinLink string `json:"joinLink"`
392
}
393
type Response struct {
394
AppPid int `json:"appPid"`
395
JoinLink string `json:"joinLink"`
396
Projects []Projects `json:"projects"`
397
}
398
type JoinLinkResponse struct {
399
AppPid int `json:"appPid"`
400
JoinLink string `json:"joinLink"`
401
402
// IDEVersion is the ideVersionHint that required by Toolbox to `setAutoConnectOnEnvironmentReady`
403
IDEVersion string `json:"ideVersion"`
404
// ProjectPath is the projectPathHint that required by Toolbox to `setAutoConnectOnEnvironmentReady`
405
ProjectPath string `json:"projectPath"`
406
}
407
408
func resolveToolboxLink(wsInfo *supervisor.WorkspaceInfoResponse) (string, error) {
409
gitpodUrl, err := url.Parse(wsInfo.GitpodHost)
410
if err != nil {
411
return "", err
412
}
413
debugWorkspace := wsInfo.DebugWorkspaceType != supervisor.DebugWorkspaceType_noDebug
414
link := url.URL{
415
Scheme: "jetbrains",
416
Host: "gateway",
417
Path: "io.gitpod.toolbox.gateway/open-in-toolbox",
418
RawQuery: fmt.Sprintf("host=%s&workspaceId=%s&debugWorkspace=%t", gitpodUrl.Hostname(), wsInfo.WorkspaceId, debugWorkspace),
419
}
420
return link.String(), nil
421
}
422
423
func resolveGatewayLink(backendPort string, wsInfo *supervisor.WorkspaceInfoResponse) (string, error) {
424
gitpodUrl, err := url.Parse(wsInfo.GitpodHost)
425
if err != nil {
426
return "", err
427
}
428
debugWorkspace := wsInfo.DebugWorkspaceType != supervisor.DebugWorkspaceType_noDebug
429
link := url.URL{
430
Scheme: "jetbrains-gateway",
431
Host: "connect",
432
Fragment: fmt.Sprintf("gitpodHost=%s&workspaceId=%s&backendPort=%s&debugWorkspace=%t", gitpodUrl.Hostname(), wsInfo.WorkspaceId, backendPort, debugWorkspace),
433
}
434
return link.String(), nil
435
}
436
437
func resolveJsonLink(backendPort string) (string, error) {
438
var (
439
hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod"
440
client = http.Client{Timeout: 1 * time.Second}
441
)
442
resp, err := client.Get(hostStatusUrl)
443
if err != nil {
444
return "", err
445
}
446
defer resp.Body.Close()
447
bodyBytes, err := io.ReadAll(resp.Body)
448
if err != nil {
449
return "", err
450
}
451
if resp.StatusCode != http.StatusOK {
452
return "", xerrors.Errorf("failed to resolve project status: %s (%d)", bodyBytes, resp.StatusCode)
453
}
454
jsonResp := &Response{}
455
err = json.Unmarshal(bodyBytes, &jsonResp)
456
if err != nil {
457
return "", err
458
}
459
if len(jsonResp.Projects) > 0 {
460
return jsonResp.Projects[0].JoinLink, nil
461
}
462
return jsonResp.JoinLink, nil
463
}
464
465
func resolveJsonLink2(launchCtx *LaunchContext, backendPort string) (*JoinLinkResponse, error) {
466
var (
467
hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod"
468
client = http.Client{Timeout: 1 * time.Second}
469
)
470
resp, err := client.Get(hostStatusUrl)
471
if err != nil {
472
return nil, err
473
}
474
defer resp.Body.Close()
475
bodyBytes, err := ioutil.ReadAll(resp.Body)
476
if err != nil {
477
return nil, err
478
}
479
if resp.StatusCode != http.StatusOK {
480
return nil, xerrors.Errorf("failed to resolve project status: %s (%d)", bodyBytes, resp.StatusCode)
481
}
482
jsonResp := &Response{}
483
err = json.Unmarshal(bodyBytes, &jsonResp)
484
if err != nil {
485
return nil, err
486
}
487
if len(jsonResp.Projects) > 0 {
488
return launchCtx.getCommonJoinLinkResponse(jsonResp.AppPid, jsonResp.Projects[0].JoinLink), nil
489
}
490
if len(jsonResp.JoinLink) > 0 {
491
return launchCtx.getCommonJoinLinkResponse(jsonResp.AppPid, jsonResp.JoinLink), nil
492
}
493
log.Error("failed to resolve JetBrains JoinLink")
494
return nil, xerrors.Errorf("failed to resolve JoinLink")
495
}
496
497
func terminateIDE(backendPort string) error {
498
var (
499
hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod&exit=true"
500
client = http.Client{Timeout: 10 * time.Second}
501
)
502
resp, err := client.Get(hostStatusUrl)
503
if err != nil {
504
return err
505
}
506
defer resp.Body.Close()
507
bodyBytes, err := ioutil.ReadAll(resp.Body)
508
if err != nil {
509
return err
510
}
511
if resp.StatusCode != http.StatusOK {
512
return xerrors.Errorf("failed to resolve terminate IDE: %s (%d)", bodyBytes, resp.StatusCode)
513
}
514
return nil
515
}
516
517
func resolveWorkspaceInfo(ctx context.Context) (*supervisor.WorkspaceInfoResponse, error) {
518
resolve := func(ctx context.Context) (wsInfo *supervisor.WorkspaceInfoResponse, err error) {
519
supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))
520
if err != nil {
521
err = errors.New("dial supervisor failed: " + err.Error())
522
return
523
}
524
defer supervisorConn.Close()
525
if wsInfo, err = supervisor.NewInfoServiceClient(supervisorConn).WorkspaceInfo(ctx, &supervisor.WorkspaceInfoRequest{}); err != nil {
526
err = errors.New("get workspace info failed: " + err.Error())
527
return
528
}
529
return
530
}
531
// try resolve workspace info 10 times
532
for attempt := 0; attempt < 10; attempt++ {
533
if wsInfo, err := resolve(ctx); err != nil {
534
log.WithError(err).Error("resolve workspace info failed")
535
time.Sleep(1 * time.Second)
536
} else {
537
return wsInfo, err
538
}
539
}
540
return nil, errors.New("failed with attempt 10 times")
541
}
542
543
func launch(launchCtx *LaunchContext) {
544
projectDir := launchCtx.wsInfo.GetCheckoutLocation()
545
gitpodConfig, err := parseGitpodConfig(projectDir)
546
if err != nil {
547
log.WithError(err).Error("failed to parse .gitpod.yml")
548
}
549
550
// configure vmoptions
551
idePrefix := launchCtx.alias
552
if launchCtx.alias == "intellij" {
553
idePrefix = "idea"
554
}
555
// [idea64|goland64|pycharm64|phpstorm64].vmoptions
556
launchCtx.vmOptionsFile = fmt.Sprintf(launchCtx.backendDir+"/bin/%s64.vmoptions", idePrefix)
557
err = configureVMOptions(gitpodConfig, launchCtx.alias, launchCtx.vmOptionsFile)
558
if err != nil {
559
log.WithError(err).Error("failed to configure vmoptions")
560
}
561
562
var riderSolutionFile string
563
if launchCtx.alias == "rider" {
564
riderSolutionFile, err = findRiderSolutionFile(projectDir)
565
if err != nil {
566
log.WithError(err).Error("failed to find a rider solution file")
567
}
568
}
569
570
launchCtx.projectDir = projectDir
571
launchCtx.configDir = fmt.Sprintf("/workspace/.config/JetBrains%s/RemoteDev-%s", launchCtx.qualifier, launchCtx.info.ProductCode)
572
launchCtx.systemDir = fmt.Sprintf("/workspace/.cache/JetBrains%s/RemoteDev-%s", launchCtx.qualifier, launchCtx.info.ProductCode)
573
launchCtx.riderSolutionFile = riderSolutionFile
574
launchCtx.projectContextDir = resolveProjectContextDir(launchCtx)
575
576
launchCtx.platformPropertiesFile = launchCtx.backendDir + "/bin/idea.properties"
577
_, err = configurePlatformProperties(launchCtx.platformPropertiesFile, launchCtx.configDir, launchCtx.systemDir)
578
if err != nil {
579
log.WithError(err).Error("failed to update platform properties file")
580
}
581
582
_, err = syncInitialContent(launchCtx, Options)
583
if err != nil {
584
log.WithError(err).Error("failed to sync initial options")
585
}
586
587
launchCtx.env = resolveLaunchContextEnv()
588
589
_, err = syncInitialContent(launchCtx, Plugins)
590
if err != nil {
591
log.WithError(err).Error("failed to sync initial plugins")
592
}
593
594
// install project plugins
595
err = installPlugins(gitpodConfig, launchCtx)
596
installPluginsCost := time.Now().Local().Sub(launchCtx.startTime).Milliseconds()
597
if err != nil {
598
log.WithError(err).WithField("cost", installPluginsCost).Error("installing repo plugins: done")
599
} else {
600
log.WithField("cost", installPluginsCost).Info("installing repo plugins: done")
601
}
602
603
// install gitpod plugin
604
err = linkRemotePlugin(launchCtx)
605
if err != nil {
606
log.WithError(err).Error("failed to install gitpod-remote plugin")
607
}
608
609
// run backend
610
run(launchCtx)
611
}
612
613
func run(launchCtx *LaunchContext) {
614
var args []string
615
if launchCtx.warmup {
616
args = append(args, "warmup")
617
} else if launchCtx.preferToolbox {
618
args = append(args, "serverMode")
619
} else {
620
args = append(args, "run")
621
}
622
args = append(args, launchCtx.projectContextDir)
623
624
cmd := remoteDevServerCmd(args, launchCtx)
625
cmd.Env = append(cmd.Env, "JETBRAINS_GITPOD_BACKEND_KIND="+launchCtx.alias)
626
workspaceUrl, err := url.Parse(launchCtx.wsInfo.WorkspaceUrl)
627
if err == nil {
628
cmd.Env = append(cmd.Env, "JETBRAINS_GITPOD_WORKSPACE_HOST="+workspaceUrl.Hostname())
629
}
630
// Enable host status endpoint
631
cmd.Env = append(cmd.Env, "CWM_HOST_STATUS_OVER_HTTP_TOKEN=gitpod")
632
633
if err := cmd.Start(); err != nil {
634
log.WithError(err).Error("failed to start")
635
}
636
637
// Nicely handle SIGTERM sinal
638
go handleSignal()
639
640
if err := cmd.Wait(); err != nil {
641
log.WithError(err).Error("failed to wait")
642
}
643
log.Info("IDE stopped, exiting")
644
os.Exit(cmd.ProcessState.ExitCode())
645
}
646
647
// resolveUserEnvs emulats the interactive login shell to ensure that all user defined shell scripts are loaded
648
func resolveUserEnvs() (userEnvs []string, err error) {
649
shell := os.Getenv("SHELL")
650
if shell == "" {
651
shell = "/bin/bash"
652
}
653
mark, err := uuid.NewRandom()
654
if err != nil {
655
return
656
}
657
658
self, err := os.Executable()
659
if err != nil {
660
return
661
}
662
envCmd := exec.Command(shell, []string{"-i", "-l", "-c", strings.Join([]string{self, "env", mark.String()}, " ")}...)
663
envCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
664
envCmd.Stderr = os.Stderr
665
envCmd.WaitDelay = 3 * time.Second
666
time.AfterFunc(8*time.Second, func() {
667
_ = syscall.Kill(-envCmd.Process.Pid, syscall.SIGKILL)
668
})
669
670
output, err := envCmd.Output()
671
if errors.Is(err, exec.ErrWaitDelay) {
672
// For some reason the command doesn't close it's I/O pipes but it already run successfully
673
// so just ignore this error
674
log.Warn("WaitDelay expired before envCmd I/O completed")
675
} else if err != nil {
676
return
677
}
678
679
markByte := []byte(mark.String())
680
start := bytes.Index(output, markByte)
681
if start == -1 {
682
err = fmt.Errorf("no %s in output", mark.String())
683
return
684
}
685
start = start + len(markByte)
686
if start > len(output) {
687
err = fmt.Errorf("no %s in output", mark.String())
688
return
689
}
690
end := bytes.LastIndex(output, markByte)
691
if end == -1 {
692
err = fmt.Errorf("no %s in output", mark.String())
693
return
694
}
695
err = json.Unmarshal(output[start:end], &userEnvs)
696
return
697
}
698
699
func resolveLaunchContextEnv() []string {
700
var launchCtxEnv []string
701
userEnvs, err := resolveUserEnvs()
702
if err == nil {
703
launchCtxEnv = append(launchCtxEnv, userEnvs...)
704
} else {
705
log.WithError(err).Error("failed to resolve user env vars")
706
launchCtxEnv = os.Environ()
707
}
708
709
// instead put them into /ide-desktop/${alias}${qualifier}/backend/bin/idea64.vmoptions
710
// otherwise JB will complain to a user on each startup
711
// by default remote dev already set -Xmx2048m, see /ide-desktop/${alias}${qualifier}/backend/plugins/remote-dev-server/bin/launcher.sh
712
launchCtxEnv = append(launchCtxEnv, "INTELLIJ_ORIGINAL_ENV_JAVA_TOOL_OPTIONS="+os.Getenv("JAVA_TOOL_OPTIONS"))
713
launchCtxEnv = append(launchCtxEnv, "JAVA_TOOL_OPTIONS=")
714
715
// Force it to be disabled as we update platform properties file already
716
// TODO: Some ides have it enabled by default still, check pycharm and remove next release
717
launchCtxEnv = append(launchCtxEnv, "REMOTE_DEV_LEGACY_PER_PROJECT_CONFIGS=0")
718
719
log.WithField("env", strings.Join(launchCtxEnv, "\n")).Info("resolved launch env")
720
721
return launchCtxEnv
722
}
723
724
func remoteDevServerCmd(args []string, launchCtx *LaunchContext) *exec.Cmd {
725
cmd := exec.Command(launchCtx.backendDir+"/bin/remote-dev-server.sh", args...)
726
cmd.Env = launchCtx.env
727
cmd.Stderr = os.Stderr
728
cmd.Stdout = os.Stdout
729
return cmd
730
}
731
732
func handleSignal() {
733
sigChan := make(chan os.Signal, 1)
734
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
735
736
<-sigChan
737
log.WithField("port", defaultBackendPort).Info("receive SIGTERM signal, terminating IDE")
738
if err := terminateIDE(defaultBackendPort); err != nil {
739
log.WithError(err).Error("failed to terminate IDE")
740
}
741
log.Info("asked IDE to terminate")
742
}
743
744
func configurePlatformProperties(platformOptionsPath string, configDir string, systemDir string) (bool, error) {
745
buffer, err := os.ReadFile(platformOptionsPath)
746
if err != nil {
747
return false, err
748
}
749
750
content := string(buffer)
751
752
updated, content := updatePlatformProperties(content, configDir, systemDir)
753
754
if updated {
755
return true, os.WriteFile(platformOptionsPath, []byte(content), 0)
756
}
757
758
return false, nil
759
}
760
761
func updatePlatformProperties(content string, configDir string, systemDir string) (bool, string) {
762
lines := strings.Split(content, "\n")
763
configMap := make(map[string]bool)
764
for _, v := range lines {
765
v = strings.TrimSpace(v)
766
if v != "" && !strings.HasPrefix(v, "#") {
767
key, _, found := strings.Cut(v, "=")
768
if found {
769
configMap[key] = true
770
}
771
}
772
}
773
774
updated := false
775
776
if _, found := configMap["idea.config.path"]; !found {
777
updated = true
778
content = strings.Join([]string{
779
content,
780
fmt.Sprintf("idea.config.path=%s", configDir),
781
fmt.Sprintf("idea.plugins.path=%s", configDir+"/plugins"),
782
fmt.Sprintf("idea.system.path=%s", systemDir),
783
fmt.Sprintf("idea.log.path=%s", systemDir+"/log"),
784
}, "\n")
785
}
786
787
return updated, content
788
}
789
790
func configureVMOptions(config *gitpod.GitpodConfig, alias string, vmOptionsPath string) error {
791
options, err := readVMOptions(vmOptionsPath)
792
if err != nil {
793
return err
794
}
795
newOptions := updateVMOptions(config, alias, options)
796
return writeVMOptions(vmOptionsPath, newOptions)
797
}
798
799
func readVMOptions(vmOptionsPath string) ([]string, error) {
800
content, err := os.ReadFile(vmOptionsPath)
801
if err != nil {
802
return nil, err
803
}
804
return strings.Fields(string(content)), nil
805
}
806
807
func writeVMOptions(vmOptionsPath string, vmoptions []string) error {
808
// vmoptions file should end with a newline
809
content := strings.Join(vmoptions, "\n") + "\n"
810
return os.WriteFile(vmOptionsPath, []byte(content), 0)
811
}
812
813
// deduplicateVMOption append new VMOptions onto old VMOptions and remove any duplicated leftmost options
814
func deduplicateVMOption(oldLines []string, newLines []string, predicate func(l, r string) bool) []string {
815
var result []string
816
var merged = append(oldLines, newLines...)
817
for i, left := range merged {
818
for _, right := range merged[i+1:] {
819
if predicate(left, right) {
820
left = ""
821
break
822
}
823
}
824
if left != "" {
825
result = append(result, left)
826
}
827
}
828
return result
829
}
830
831
func updateVMOptions(
832
config *gitpod.GitpodConfig,
833
alias string,
834
// original vmoptions (inherited from $JETBRAINS_IDE_HOME/bin/idea64.vmoptions)
835
ideaVMOptionsLines []string) []string {
836
// inspired by how intellij platform merge the VMOptions
837
// https://github.com/JetBrains/intellij-community/blob/master/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java#L1115
838
filterFunc := func(l, r string) bool {
839
isEqual := l == r
840
isXmx := strings.HasPrefix(l, "-Xmx") && strings.HasPrefix(r, "-Xmx")
841
isXms := strings.HasPrefix(l, "-Xms") && strings.HasPrefix(r, "-Xms")
842
isXss := strings.HasPrefix(l, "-Xss") && strings.HasPrefix(r, "-Xss")
843
isXXOptions := strings.HasPrefix(l, "-XX:") && strings.HasPrefix(r, "-XX:") &&
844
strings.Split(l, "=")[0] == strings.Split(r, "=")[0]
845
return isEqual || isXmx || isXms || isXss || isXXOptions
846
}
847
// Gitpod's default customization
848
var gitpodVMOptions []string
849
gitpodVMOptions = append(gitpodVMOptions, "-Dgtw.disable.exit.dialog=true")
850
// temporary disable auto-attach of the async-profiler to prevent JVM crash
851
// see https://youtrack.jetbrains.com/issue/IDEA-326201/SIGSEGV-on-startup-2023.2-IDE-backend-on-gitpod.io?s=SIGSEGV-on-startup-2023.2-IDE-backend-on-gitpod.io
852
gitpodVMOptions = append(gitpodVMOptions, "-Dfreeze.reporter.profiling=false")
853
if alias == "intellij" {
854
gitpodVMOptions = append(gitpodVMOptions, "-Djdk.configure.existing=true")
855
}
856
// container relevant options
857
gitpodVMOptions = append(gitpodVMOptions, "-XX:+UseContainerSupport")
858
cpuCount := os.Getenv("GITPOD_CPU_COUNT")
859
parsedCPUCount, err := strconv.Atoi(cpuCount)
860
// if CPU count is set and is parseable as a positive number
861
if err == nil && parsedCPUCount > 0 && parsedCPUCount <= 16 {
862
gitpodVMOptions = append(gitpodVMOptions, "-XX:ActiveProcessorCount="+cpuCount)
863
}
864
865
memory := os.Getenv("GITPOD_MEMORY")
866
parsedMemory, err := strconv.Atoi(memory)
867
if err == nil && parsedMemory > 0 {
868
xmx := (float64(parsedMemory) * 0.6)
869
if xmx > maxDefaultXmx { // 8G
870
xmx = maxDefaultXmx
871
}
872
if xmx > minDefaultXmx {
873
gitpodVMOptions = append(gitpodVMOptions, fmt.Sprintf("-Xmx%dm", int(xmx)))
874
}
875
}
876
877
vmoptions := deduplicateVMOption(ideaVMOptionsLines, gitpodVMOptions, filterFunc)
878
879
// user-defined vmoptions (EnvVar)
880
userVMOptionsVar := os.Getenv(strings.ToUpper(alias) + "_VMOPTIONS")
881
userVMOptions := strings.Fields(userVMOptionsVar)
882
if len(userVMOptions) > 0 {
883
vmoptions = deduplicateVMOption(vmoptions, userVMOptions, filterFunc)
884
}
885
886
// project-defined vmoptions (.gitpod.yml)
887
if config != nil {
888
productConfig := getProductConfig(config, alias)
889
if productConfig != nil {
890
projectVMOptions := strings.Fields(productConfig.Vmoptions)
891
if len(projectVMOptions) > 0 {
892
vmoptions = deduplicateVMOption(vmoptions, projectVMOptions, filterFunc)
893
}
894
}
895
}
896
897
return vmoptions
898
}
899
900
/*
901
*
902
903
{
904
"buildNumber" : "221.4994.44",
905
"customProperties" : [ ],
906
"dataDirectoryName" : "IntelliJIdea2022.1",
907
"launch" : [ {
908
"javaExecutablePath" : "jbr/bin/java",
909
"launcherPath" : "bin/idea.sh",
910
"os" : "Linux",
911
"startupWmClass" : "jetbrains-idea",
912
"vmOptionsFilePath" : "bin/idea64.vmoptions"
913
} ],
914
"name" : "IntelliJ IDEA",
915
"productCode" : "IU",
916
"svgIconPath" : "bin/idea.svg",
917
"version" : "2022.1",
918
"versionSuffix" : "EAP"
919
}
920
*/
921
type ProductInfo struct {
922
BuildNumber string `json:"buildNumber"`
923
Version string `json:"version"`
924
ProductCode string `json:"productCode"`
925
}
926
927
func resolveProductInfo(backendDir string) (*ProductInfo, error) {
928
f, err := os.Open(backendDir + "/product-info.json")
929
if err != nil {
930
return nil, err
931
}
932
defer f.Close()
933
content, err := ioutil.ReadAll(f)
934
if err != nil {
935
return nil, err
936
}
937
938
var info ProductInfo
939
err = json.Unmarshal(content, &info)
940
return &info, err
941
}
942
943
type SyncTarget string
944
945
const (
946
Options SyncTarget = "options"
947
Plugins SyncTarget = "plugins"
948
)
949
950
func syncInitialContent(launchCtx *LaunchContext, target SyncTarget) (bool, error) {
951
destDir, err, alreadySynced := ensureInitialSyncDest(launchCtx, target)
952
if alreadySynced {
953
log.Infof("initial %s is already synced, skipping", target)
954
return alreadySynced, nil
955
}
956
if err != nil {
957
return alreadySynced, err
958
}
959
960
srcDirs, err := collectSyncSources(launchCtx, target)
961
if err != nil {
962
return alreadySynced, err
963
}
964
if len(srcDirs) == 0 {
965
// nothing to sync
966
return alreadySynced, nil
967
}
968
969
for _, srcDir := range srcDirs {
970
if target == Plugins {
971
files, err := ioutil.ReadDir(srcDir)
972
if err != nil {
973
return alreadySynced, err
974
}
975
976
for _, file := range files {
977
err := syncPlugin(file, srcDir, destDir)
978
if err != nil {
979
log.WithError(err).WithField("file", file.Name()).WithField("srcDir", srcDir).WithField("destDir", destDir).Error("failed to sync plugin")
980
}
981
}
982
} else {
983
cp := exec.Command("cp", "-rf", srcDir+"/.", destDir)
984
err = cp.Run()
985
if err != nil {
986
return alreadySynced, err
987
}
988
}
989
}
990
return alreadySynced, nil
991
}
992
993
func syncPlugin(file fs.FileInfo, srcDir, destDir string) error {
994
if file.IsDir() {
995
_, err := os.Stat(filepath.Join(destDir, file.Name()))
996
if !os.IsNotExist(err) {
997
log.WithField("plugin", file.Name()).Info("plugin is already synced, skipping")
998
return nil
999
}
1000
return exec.Command("cp", "-rf", filepath.Join(srcDir, file.Name()), destDir).Run()
1001
}
1002
if filepath.Ext(file.Name()) != ".zip" {
1003
return nil
1004
}
1005
archiveFile := filepath.Join(srcDir, file.Name())
1006
rootDir, err := getRootDirFromArchive(archiveFile)
1007
if err != nil {
1008
return err
1009
}
1010
_, err = os.Stat(filepath.Join(destDir, rootDir))
1011
if !os.IsNotExist(err) {
1012
log.WithField("plugin", rootDir).Info("plugin is already synced, skipping")
1013
return nil
1014
}
1015
return unzipArchive(archiveFile, destDir)
1016
}
1017
1018
func ensureInitialSyncDest(launchCtx *LaunchContext, target SyncTarget) (string, error, bool) {
1019
targetDestDir := launchCtx.configDir
1020
if target == Plugins {
1021
targetDestDir = launchCtx.backendDir
1022
}
1023
destDir := fmt.Sprintf("%s/%s", targetDestDir, target)
1024
if target == Options {
1025
_, err := os.Stat(destDir)
1026
if !os.IsNotExist(err) {
1027
return "", nil, true
1028
}
1029
err = os.MkdirAll(destDir, os.ModePerm)
1030
if err != nil {
1031
return "", err, false
1032
}
1033
}
1034
return destDir, nil, false
1035
}
1036
1037
func collectSyncSources(launchCtx *LaunchContext, target SyncTarget) ([]string, error) {
1038
userHomeDir, err := os.UserHomeDir()
1039
if err != nil {
1040
return nil, err
1041
}
1042
var srcDirs []string
1043
for _, srcDir := range []string{
1044
fmt.Sprintf("%s/.gitpod/jetbrains/%s", userHomeDir, target),
1045
fmt.Sprintf("%s/.gitpod/jetbrains/%s/%s", userHomeDir, launchCtx.alias, target),
1046
fmt.Sprintf("%s/.gitpod/jetbrains/%s", launchCtx.projectDir, target),
1047
fmt.Sprintf("%s/.gitpod/jetbrains/%s/%s", launchCtx.projectDir, launchCtx.alias, target),
1048
} {
1049
srcStat, err := os.Stat(srcDir)
1050
if os.IsNotExist(err) {
1051
// nothing to sync
1052
continue
1053
}
1054
if err != nil {
1055
return nil, err
1056
}
1057
if !srcStat.IsDir() {
1058
return nil, fmt.Errorf("%s is not a directory", srcDir)
1059
}
1060
srcDirs = append(srcDirs, srcDir)
1061
}
1062
return srcDirs, nil
1063
}
1064
1065
func getRootDirFromArchive(zipPath string) (string, error) {
1066
r, err := zip.OpenReader(zipPath)
1067
if err != nil {
1068
return "", err
1069
}
1070
defer r.Close()
1071
1072
if len(r.File) == 0 {
1073
return "", fmt.Errorf("empty archive")
1074
}
1075
1076
// Assuming the first file in the zip is the root directory or a file in the root directory
1077
return strings.SplitN(r.File[0].Name, "/", 2)[0], nil
1078
}
1079
1080
func unzipArchive(src, dest string) error {
1081
r, err := zip.OpenReader(src)
1082
if err != nil {
1083
return err
1084
}
1085
defer r.Close()
1086
1087
for _, f := range r.File {
1088
rc, err := f.Open()
1089
if err != nil {
1090
return err
1091
}
1092
defer rc.Close()
1093
1094
fpath := filepath.Join(dest, f.Name)
1095
if f.FileInfo().IsDir() {
1096
err := os.MkdirAll(fpath, os.ModePerm)
1097
if err != nil {
1098
return err
1099
}
1100
} else {
1101
fdir := filepath.Dir(fpath)
1102
err := os.MkdirAll(fdir, os.ModePerm)
1103
if err != nil {
1104
return err
1105
}
1106
1107
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
1108
if err != nil {
1109
return err
1110
}
1111
defer outFile.Close()
1112
1113
_, err = io.Copy(outFile, rc)
1114
if err != nil {
1115
return err
1116
}
1117
}
1118
}
1119
return nil
1120
}
1121
1122
func installPlugins(config *gitpod.GitpodConfig, launchCtx *LaunchContext) error {
1123
plugins, err := getPlugins(config, launchCtx.alias)
1124
if err != nil {
1125
return err
1126
}
1127
if len(plugins) <= 0 {
1128
return nil
1129
}
1130
1131
var args []string
1132
args = append(args, "installPlugins")
1133
args = append(args, launchCtx.projectContextDir)
1134
args = append(args, plugins...)
1135
cmd := remoteDevServerCmd(args, launchCtx)
1136
installErr := cmd.Run()
1137
1138
// delete alien_plugins.txt to suppress 3rd-party plugins consent on startup to workaround backend startup freeze
1139
err = os.Remove(launchCtx.configDir + "/alien_plugins.txt")
1140
if err != nil && !os.IsNotExist(err) && !strings.Contains(err.Error(), "no such file or directory") {
1141
log.WithError(err).Error("failed to suppress 3rd-party plugins consent")
1142
}
1143
1144
if installErr != nil {
1145
return errors.New("failed to install repo plugins: " + installErr.Error())
1146
}
1147
return nil
1148
}
1149
1150
func parseGitpodConfig(repoRoot string) (*gitpod.GitpodConfig, error) {
1151
if repoRoot == "" {
1152
return nil, errors.New("repoRoot is empty")
1153
}
1154
data, err := os.ReadFile(filepath.Join(repoRoot, ".gitpod.yml"))
1155
if err != nil {
1156
// .gitpod.yml not exist is ok
1157
if errors.Is(err, os.ErrNotExist) {
1158
return nil, nil
1159
}
1160
return nil, errors.New("read .gitpod.yml file failed: " + err.Error())
1161
}
1162
var config *gitpod.GitpodConfig
1163
if err = yaml.Unmarshal(data, &config); err != nil {
1164
return nil, errors.New("unmarshal .gitpod.yml file failed" + err.Error())
1165
}
1166
return config, nil
1167
}
1168
1169
func getPlugins(config *gitpod.GitpodConfig, alias string) ([]string, error) {
1170
var plugins []string
1171
if config == nil || config.Jetbrains == nil {
1172
return nil, nil
1173
}
1174
if config.Jetbrains.Plugins != nil {
1175
plugins = append(plugins, config.Jetbrains.Plugins...)
1176
}
1177
productConfig := getProductConfig(config, alias)
1178
if productConfig != nil && productConfig.Plugins != nil {
1179
plugins = append(plugins, productConfig.Plugins...)
1180
}
1181
return plugins, nil
1182
}
1183
1184
func getProductConfig(config *gitpod.GitpodConfig, alias string) *gitpod.JetbrainsProduct {
1185
defer func() {
1186
if err := recover(); err != nil {
1187
log.WithField("error", err).WithField("alias", alias).Error("failed to extract JB product config")
1188
}
1189
}()
1190
v := reflect.ValueOf(*config.Jetbrains).FieldByNameFunc(func(s string) bool {
1191
return strings.ToLower(s) == alias
1192
}).Interface()
1193
productConfig, ok := v.(*gitpod.JetbrainsProduct)
1194
if !ok {
1195
return nil
1196
}
1197
return productConfig
1198
}
1199
1200
func linkRemotePlugin(launchCtx *LaunchContext) error {
1201
remotePluginsFolder := launchCtx.configDir + "/plugins"
1202
if launchCtx.info.Version == "2022.3.3" {
1203
remotePluginsFolder = launchCtx.backendDir + "/plugins"
1204
}
1205
remotePluginDir := remotePluginsFolder + "/gitpod-remote"
1206
if err := os.MkdirAll(remotePluginsFolder, 0755); err != nil {
1207
return err
1208
}
1209
1210
// added for backwards compatibility, can be removed in the future
1211
sourceDir := "/ide-desktop-plugins/gitpod-remote-" + os.Getenv("JETBRAINS_BACKEND_QUALIFIER")
1212
_, err := os.Stat(sourceDir)
1213
if err == nil {
1214
return safeLink(sourceDir, remotePluginDir)
1215
}
1216
1217
return safeLink("/ide-desktop-plugins/gitpod-remote", remotePluginDir)
1218
}
1219
1220
// safeLink creates a symlink from source to target, removing the old target if it exists
1221
func safeLink(source, target string) error {
1222
if _, err := os.Lstat(target); err == nil {
1223
// unlink the old symlink
1224
if err2 := os.RemoveAll(target); err2 != nil {
1225
log.WithError(err).Error("failed to unlink old symlink")
1226
}
1227
}
1228
return os.Symlink(source, target)
1229
}
1230
1231
// 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
1232
func findRiderSolutionFile(root string) (string, error) {
1233
slnRegEx := regexp.MustCompile(`^.+\.sln$`)
1234
projRegEx := regexp.MustCompile(`^.+\.csproj$`)
1235
1236
var slnFiles []string
1237
var csprojFiles []string
1238
1239
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
1240
if err != nil {
1241
return err
1242
} else if slnRegEx.MatchString(info.Name()) {
1243
slnFiles = append(slnFiles, path)
1244
} else if projRegEx.MatchString(info.Name()) {
1245
csprojFiles = append(csprojFiles, path)
1246
}
1247
return nil
1248
})
1249
1250
if err != nil {
1251
return "", err
1252
}
1253
1254
if len(slnFiles) > 0 {
1255
return slnFiles[0], nil
1256
} else if len(csprojFiles) > 0 {
1257
return csprojFiles[0], nil
1258
}
1259
1260
return root, nil
1261
}
1262
1263
func resolveProjectContextDir(launchCtx *LaunchContext) string {
1264
if launchCtx.alias == "rider" {
1265
return launchCtx.riderSolutionFile
1266
}
1267
1268
return launchCtx.projectDir
1269
}
1270
1271
func configureToolboxCliProperties(backendDir string) error {
1272
userHomeDir, err := os.UserHomeDir()
1273
if err != nil {
1274
return err
1275
}
1276
1277
toolboxCliPropertiesDir := fmt.Sprintf("%s/.local/share/JetBrains/Toolbox", userHomeDir)
1278
_, err = os.Stat(toolboxCliPropertiesDir)
1279
if !os.IsNotExist(err) {
1280
return err
1281
}
1282
err = os.MkdirAll(toolboxCliPropertiesDir, os.ModePerm)
1283
if err != nil {
1284
return err
1285
}
1286
1287
toolboxCliPropertiesFilePath := fmt.Sprintf("%s/environment.json", toolboxCliPropertiesDir)
1288
1289
debuggingToolbox := os.Getenv("GITPOD_TOOLBOX_DEBUGGING")
1290
allowInstallation := strconv.FormatBool(strings.Contains(debuggingToolbox, "allowInstallation"))
1291
1292
// TODO(hw): restrict IDE installation
1293
content := fmt.Sprintf(`{
1294
"tools": {
1295
"allowInstallation": %s,
1296
"allowUpdate": false,
1297
"allowUninstallation": %s,
1298
"location": [
1299
{
1300
"path": "%s"
1301
}
1302
]
1303
}
1304
}`, allowInstallation, allowInstallation, backendDir)
1305
1306
return os.WriteFile(toolboxCliPropertiesFilePath, []byte(content), 0o644)
1307
}
1308
1309