Path: blob/main/components/ide/jetbrains/launcher/main.go
2501 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package main56import (7"archive/zip"8"bytes"9"context"10"encoding/json"11"errors"12"fmt"13"io"14"io/fs"15"io/ioutil"16"net"17"net/http"18"net/url"19"os"20"os/exec"21"os/signal"22"path/filepath"23"reflect"24"regexp"25"strconv"26"strings"27"syscall"28"time"2930"github.com/google/uuid"31"github.com/hashicorp/go-version"32"golang.org/x/xerrors"33"google.golang.org/grpc"34"google.golang.org/grpc/credentials/insecure"35yaml "gopkg.in/yaml.v2"3637"github.com/gitpod-io/gitpod/common-go/log"38"github.com/gitpod-io/gitpod/common-go/util"39gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"40"github.com/gitpod-io/gitpod/jetbrains/launcher/pkg/constant"41supervisor "github.com/gitpod-io/gitpod/supervisor/api"42)4344const (45defaultBackendPort = "63342"46maxDefaultXmx = 8 * 102447minDefaultXmx = 2 * 102448)4950var (51// ServiceName is the name we use for tracing/logging.52ServiceName = "jetbrains-launcher"53)5455type LaunchContext struct {56startTime time.Time5758port string59alias string60label string61warmup bool6263preferToolbox bool64qualifier string65productDir string66backendDir string67info *ProductInfo68backendVersion *version.Version69wsInfo *supervisor.WorkspaceInfoResponse7071vmOptionsFile string72platformPropertiesFile string73projectDir string74configDir string75systemDir string76projectContextDir string77riderSolutionFile string7879env []string8081// Custom fields8283// shouldWaitBackendPlugin is controlled by env GITPOD_WAIT_IDE_BACKEND84shouldWaitBackendPlugin bool85}8687func (c *LaunchContext) getCommonJoinLinkResponse(appPid int, joinLink string) *JoinLinkResponse {88return &JoinLinkResponse{89AppPid: appPid,90JoinLink: joinLink,91IDEVersion: fmt.Sprintf("%s-%s", c.info.ProductCode, c.info.BuildNumber),92ProjectPath: c.projectContextDir,93}94}9596// JB startup entrypoint97func main() {98if len(os.Args) == 3 && os.Args[1] == "env" && os.Args[2] != "" {99var mark = os.Args[2]100content, err := json.Marshal(os.Environ())101exitStatus := 0102if err != nil {103fmt.Fprintf(os.Stderr, "%s", err)104exitStatus = 1105}106fmt.Printf("%s%s%s", mark, content, mark)107os.Exit(exitStatus)108return109}110111// supervisor refer see https://github.com/gitpod-io/gitpod/blob/main/components/supervisor/pkg/supervisor/supervisor.go#L961112shouldWaitBackendPlugin := os.Getenv("GITPOD_WAIT_IDE_BACKEND") == "true"113debugEnabled := os.Getenv("SUPERVISOR_DEBUG_ENABLE") == "true"114preferToolbox := os.Getenv("GITPOD_PREFER_TOOLBOX") == "true"115log.Init(ServiceName, constant.Version, true, debugEnabled)116log.Info(ServiceName + ": " + constant.Version)117startTime := time.Now()118119log.WithField("shouldWait", shouldWaitBackendPlugin).Info("should wait backend plugin")120var port string121var warmup bool122123if len(os.Args) < 2 {124log.Fatalf("Usage: %s (warmup|<port>)\n", os.Args[0])125}126127if os.Args[1] == "warmup" {128if len(os.Args) < 3 {129log.Fatalf("Usage: %s %s <alias>\n", os.Args[0], os.Args[1])130}131132warmup = true133} else {134if len(os.Args) < 3 {135log.Fatalf("Usage: %s <port> <kind> [<link label>]\n", os.Args[0])136}137138port = os.Args[1]139}140141alias := os.Args[2]142label := "Open JetBrains IDE"143if len(os.Args) > 3 {144label = os.Args[3]145}146147qualifier := os.Getenv("JETBRAINS_BACKEND_QUALIFIER")148if qualifier == "stable" {149qualifier = ""150} else {151qualifier = "-" + qualifier152}153productDir := "/ide-desktop/" + alias + qualifier154backendDir := productDir + "/backend"155156info, err := resolveProductInfo(backendDir)157if err != nil {158log.WithError(err).Error("failed to resolve product info")159return160}161162backendVersion, err := version.NewVersion(info.Version)163if err != nil {164log.WithError(err).Error("failed to resolve backend version")165return166}167168wsInfo, err := resolveWorkspaceInfo(context.Background())169if err != nil || wsInfo == nil {170log.WithError(err).WithField("wsInfo", wsInfo).Error("resolve workspace info failed")171return172}173174launchCtx := &LaunchContext{175startTime: startTime,176177warmup: warmup,178port: port,179alias: alias,180label: label,181182preferToolbox: preferToolbox,183qualifier: qualifier,184productDir: productDir,185backendDir: backendDir,186info: info,187backendVersion: backendVersion,188wsInfo: wsInfo,189shouldWaitBackendPlugin: shouldWaitBackendPlugin,190}191192if launchCtx.warmup {193launch(launchCtx)194return195}196197if preferToolbox {198err = configureToolboxCliProperties(backendDir)199if err != nil {200log.WithError(err).Error("failed to write toolbox cli config file")201return202}203}204205// we should start serving immediately and postpone launch206// in order to enable a JB Gateway to connect as soon as possible207go launch(launchCtx)208// IMPORTANT: don't put startup logic in serve!!!209serve(launchCtx)210}211212func serve(launchCtx *LaunchContext) {213debugAgentPrefix := "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:"214http.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) {215options, err := readVMOptions(launchCtx.vmOptionsFile)216if err != nil {217log.WithError(err).Error("failed to configure debug agent")218http.Error(w, err.Error(), http.StatusInternalServerError)219return220}221debugPort := ""222i := len(options) - 1223for i >= 0 && debugPort == "" {224option := options[i]225if strings.HasPrefix(option, debugAgentPrefix) {226debugPort = option[len(debugAgentPrefix):]227if debugPort == "0" {228debugPort = ""229}230}231i--232}233234if debugPort != "" {235fmt.Fprint(w, debugPort)236return237}238netListener, err := net.Listen("tcp", "localhost:0")239if err != nil {240log.WithError(err).Error("failed to configure debug agent")241http.Error(w, err.Error(), http.StatusInternalServerError)242return243}244debugPort = strconv.Itoa(netListener.(*net.TCPListener).Addr().(*net.TCPAddr).Port)245_ = netListener.Close()246247debugOptions := []string{debugAgentPrefix + debugPort}248options = deduplicateVMOption(options, debugOptions, func(l, r string) bool {249return strings.HasPrefix(l, debugAgentPrefix) && strings.HasPrefix(r, debugAgentPrefix)250})251err = writeVMOptions(launchCtx.vmOptionsFile, options)252if err != nil {253log.WithError(err).Error("failed to configure debug agent")254http.Error(w, err.Error(), http.StatusInternalServerError)255return256}257fmt.Fprint(w, debugPort)258restart(r)259})260http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) {261fmt.Fprint(w, "terminated")262restart(r)263})264http.HandleFunc("/joinLink", func(w http.ResponseWriter, r *http.Request) {265backendPort := r.URL.Query().Get("backendPort")266if backendPort == "" {267backendPort = defaultBackendPort268}269jsonLink, err := resolveJsonLink(backendPort)270if err != nil {271log.WithError(err).Error("cannot resolve join link")272http.Error(w, err.Error(), http.StatusServiceUnavailable)273return274}275fmt.Fprint(w, jsonLink)276})277http.HandleFunc("/joinLink2", func(w http.ResponseWriter, r *http.Request) {278backendPort := r.URL.Query().Get("backendPort")279if backendPort == "" {280backendPort = defaultBackendPort281}282jsonResp, err := resolveJsonLink2(launchCtx, backendPort)283if err != nil {284log.WithError(err).Error("cannot resolve join link")285http.Error(w, err.Error(), http.StatusServiceUnavailable)286return287}288json.NewEncoder(w).Encode(jsonResp)289})290http.HandleFunc("/gatewayLink", func(w http.ResponseWriter, r *http.Request) {291backendPort := r.URL.Query().Get("backendPort")292if backendPort == "" {293backendPort = defaultBackendPort294}295jsonLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo)296if err != nil {297log.WithError(err).Error("cannot resolve gateway link")298http.Error(w, err.Error(), http.StatusServiceUnavailable)299return300}301fmt.Fprint(w, jsonLink)302})303http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {304if launchCtx.preferToolbox {305response := make(map[string]string)306toolboxLink, err := resolveToolboxLink(launchCtx.wsInfo)307if err != nil {308log.WithError(err).Error("cannot resolve toolbox link")309http.Error(w, err.Error(), http.StatusServiceUnavailable)310return311}312response["link"] = toolboxLink313response["label"] = launchCtx.label314response["clientID"] = "jetbrains-toolbox"315response["kind"] = launchCtx.alias316w.Header().Set("Content-Type", "application/json")317_ = json.NewEncoder(w).Encode(response)318return319}320backendPort := r.URL.Query().Get("backendPort")321if backendPort == "" {322backendPort = defaultBackendPort323}324if err := isBackendPluginReady(r.Context(), backendPort, launchCtx.shouldWaitBackendPlugin); err != nil {325http.Error(w, err.Error(), http.StatusServiceUnavailable)326return327}328gatewayLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo)329if err != nil {330log.WithError(err).Error("cannot resolve gateway link")331http.Error(w, err.Error(), http.StatusServiceUnavailable)332return333}334response := make(map[string]string)335response["link"] = gatewayLink336response["label"] = launchCtx.label337response["clientID"] = "jetbrains-gateway"338response["kind"] = launchCtx.alias339w.Header().Set("Content-Type", "application/json")340_ = json.NewEncoder(w).Encode(response)341})342343fmt.Printf("Starting status proxy for desktop IDE at port %s\n", launchCtx.port)344if err := http.ListenAndServe(fmt.Sprintf(":%s", launchCtx.port), nil); err != nil {345log.Fatal(err)346}347}348349// isBackendPluginReady checks if the backend plugin is ready via backend plugin CLI GitpodCLIService.kt350func isBackendPluginReady(ctx context.Context, backendPort string, shouldWaitBackendPlugin bool) error {351if !shouldWaitBackendPlugin {352log.Debug("will not wait plugin ready")353return nil354}355log.WithField("backendPort", backendPort).Debug("wait backend plugin to be ready")356// Use op=metrics so that we don't need to rebuild old backend-plugin357url, err := url.Parse("http://localhost:" + backendPort + "/api/gitpod/cli?op=metrics")358if err != nil {359return err360}361req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)362if err != nil {363return err364}365resp, err := http.DefaultClient.Do(req)366if err != nil {367return err368}369defer resp.Body.Close()370if resp.StatusCode != http.StatusOK {371return fmt.Errorf("backend plugin is not ready: %d", resp.StatusCode)372}373return nil374}375376func restart(r *http.Request) {377backendPort := r.URL.Query().Get("backendPort")378if backendPort == "" {379backendPort = defaultBackendPort380}381err := terminateIDE(backendPort)382if err != nil {383log.WithError(err).Error("failed to terminate IDE gracefully")384os.Exit(1)385}386os.Exit(0)387}388389type Projects struct {390JoinLink string `json:"joinLink"`391}392type Response struct {393AppPid int `json:"appPid"`394JoinLink string `json:"joinLink"`395Projects []Projects `json:"projects"`396}397type JoinLinkResponse struct {398AppPid int `json:"appPid"`399JoinLink string `json:"joinLink"`400401// IDEVersion is the ideVersionHint that required by Toolbox to `setAutoConnectOnEnvironmentReady`402IDEVersion string `json:"ideVersion"`403// ProjectPath is the projectPathHint that required by Toolbox to `setAutoConnectOnEnvironmentReady`404ProjectPath string `json:"projectPath"`405}406407func resolveToolboxLink(wsInfo *supervisor.WorkspaceInfoResponse) (string, error) {408gitpodUrl, err := url.Parse(wsInfo.GitpodHost)409if err != nil {410return "", err411}412debugWorkspace := wsInfo.DebugWorkspaceType != supervisor.DebugWorkspaceType_noDebug413link := url.URL{414Scheme: "jetbrains",415Host: "gateway",416Path: "io.gitpod.toolbox.gateway/open-in-toolbox",417RawQuery: fmt.Sprintf("host=%s&workspaceId=%s&debugWorkspace=%t", gitpodUrl.Hostname(), wsInfo.WorkspaceId, debugWorkspace),418}419return link.String(), nil420}421422func resolveGatewayLink(backendPort string, wsInfo *supervisor.WorkspaceInfoResponse) (string, error) {423gitpodUrl, err := url.Parse(wsInfo.GitpodHost)424if err != nil {425return "", err426}427debugWorkspace := wsInfo.DebugWorkspaceType != supervisor.DebugWorkspaceType_noDebug428link := url.URL{429Scheme: "jetbrains-gateway",430Host: "connect",431Fragment: fmt.Sprintf("gitpodHost=%s&workspaceId=%s&backendPort=%s&debugWorkspace=%t", gitpodUrl.Hostname(), wsInfo.WorkspaceId, backendPort, debugWorkspace),432}433return link.String(), nil434}435436func resolveJsonLink(backendPort string) (string, error) {437var (438hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod"439client = http.Client{Timeout: 1 * time.Second}440)441resp, err := client.Get(hostStatusUrl)442if err != nil {443return "", err444}445defer resp.Body.Close()446bodyBytes, err := io.ReadAll(resp.Body)447if err != nil {448return "", err449}450if resp.StatusCode != http.StatusOK {451return "", xerrors.Errorf("failed to resolve project status: %s (%d)", bodyBytes, resp.StatusCode)452}453jsonResp := &Response{}454err = json.Unmarshal(bodyBytes, &jsonResp)455if err != nil {456return "", err457}458if len(jsonResp.Projects) > 0 {459return jsonResp.Projects[0].JoinLink, nil460}461return jsonResp.JoinLink, nil462}463464func resolveJsonLink2(launchCtx *LaunchContext, backendPort string) (*JoinLinkResponse, error) {465var (466hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod"467client = http.Client{Timeout: 1 * time.Second}468)469resp, err := client.Get(hostStatusUrl)470if err != nil {471return nil, err472}473defer resp.Body.Close()474bodyBytes, err := ioutil.ReadAll(resp.Body)475if err != nil {476return nil, err477}478if resp.StatusCode != http.StatusOK {479return nil, xerrors.Errorf("failed to resolve project status: %s (%d)", bodyBytes, resp.StatusCode)480}481jsonResp := &Response{}482err = json.Unmarshal(bodyBytes, &jsonResp)483if err != nil {484return nil, err485}486if len(jsonResp.Projects) > 0 {487return launchCtx.getCommonJoinLinkResponse(jsonResp.AppPid, jsonResp.Projects[0].JoinLink), nil488}489if len(jsonResp.JoinLink) > 0 {490return launchCtx.getCommonJoinLinkResponse(jsonResp.AppPid, jsonResp.JoinLink), nil491}492log.Error("failed to resolve JetBrains JoinLink")493return nil, xerrors.Errorf("failed to resolve JoinLink")494}495496func terminateIDE(backendPort string) error {497var (498hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod&exit=true"499client = http.Client{Timeout: 10 * time.Second}500)501resp, err := client.Get(hostStatusUrl)502if err != nil {503return err504}505defer resp.Body.Close()506bodyBytes, err := ioutil.ReadAll(resp.Body)507if err != nil {508return err509}510if resp.StatusCode != http.StatusOK {511return xerrors.Errorf("failed to resolve terminate IDE: %s (%d)", bodyBytes, resp.StatusCode)512}513return nil514}515516func resolveWorkspaceInfo(ctx context.Context) (*supervisor.WorkspaceInfoResponse, error) {517resolve := func(ctx context.Context) (wsInfo *supervisor.WorkspaceInfoResponse, err error) {518supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))519if err != nil {520err = errors.New("dial supervisor failed: " + err.Error())521return522}523defer supervisorConn.Close()524if wsInfo, err = supervisor.NewInfoServiceClient(supervisorConn).WorkspaceInfo(ctx, &supervisor.WorkspaceInfoRequest{}); err != nil {525err = errors.New("get workspace info failed: " + err.Error())526return527}528return529}530// try resolve workspace info 10 times531for attempt := 0; attempt < 10; attempt++ {532if wsInfo, err := resolve(ctx); err != nil {533log.WithError(err).Error("resolve workspace info failed")534time.Sleep(1 * time.Second)535} else {536return wsInfo, err537}538}539return nil, errors.New("failed with attempt 10 times")540}541542func launch(launchCtx *LaunchContext) {543projectDir := launchCtx.wsInfo.GetCheckoutLocation()544gitpodConfig, err := parseGitpodConfig(projectDir)545if err != nil {546log.WithError(err).Error("failed to parse .gitpod.yml")547}548549// configure vmoptions550idePrefix := launchCtx.alias551if launchCtx.alias == "intellij" {552idePrefix = "idea"553}554// [idea64|goland64|pycharm64|phpstorm64].vmoptions555launchCtx.vmOptionsFile = fmt.Sprintf(launchCtx.backendDir+"/bin/%s64.vmoptions", idePrefix)556err = configureVMOptions(gitpodConfig, launchCtx.alias, launchCtx.vmOptionsFile)557if err != nil {558log.WithError(err).Error("failed to configure vmoptions")559}560561var riderSolutionFile string562if launchCtx.alias == "rider" {563riderSolutionFile, err = findRiderSolutionFile(projectDir)564if err != nil {565log.WithError(err).Error("failed to find a rider solution file")566}567}568569launchCtx.projectDir = projectDir570launchCtx.configDir = fmt.Sprintf("/workspace/.config/JetBrains%s/RemoteDev-%s", launchCtx.qualifier, launchCtx.info.ProductCode)571launchCtx.systemDir = fmt.Sprintf("/workspace/.cache/JetBrains%s/RemoteDev-%s", launchCtx.qualifier, launchCtx.info.ProductCode)572launchCtx.riderSolutionFile = riderSolutionFile573launchCtx.projectContextDir = resolveProjectContextDir(launchCtx)574575launchCtx.platformPropertiesFile = launchCtx.backendDir + "/bin/idea.properties"576_, err = configurePlatformProperties(launchCtx.platformPropertiesFile, launchCtx.configDir, launchCtx.systemDir)577if err != nil {578log.WithError(err).Error("failed to update platform properties file")579}580581_, err = syncInitialContent(launchCtx, Options)582if err != nil {583log.WithError(err).Error("failed to sync initial options")584}585586launchCtx.env = resolveLaunchContextEnv()587588_, err = syncInitialContent(launchCtx, Plugins)589if err != nil {590log.WithError(err).Error("failed to sync initial plugins")591}592593// install project plugins594err = installPlugins(gitpodConfig, launchCtx)595installPluginsCost := time.Now().Local().Sub(launchCtx.startTime).Milliseconds()596if err != nil {597log.WithError(err).WithField("cost", installPluginsCost).Error("installing repo plugins: done")598} else {599log.WithField("cost", installPluginsCost).Info("installing repo plugins: done")600}601602// install gitpod plugin603err = linkRemotePlugin(launchCtx)604if err != nil {605log.WithError(err).Error("failed to install gitpod-remote plugin")606}607608// run backend609run(launchCtx)610}611612func run(launchCtx *LaunchContext) {613var args []string614if launchCtx.warmup {615args = append(args, "warmup")616} else if launchCtx.preferToolbox {617args = append(args, "serverMode")618} else {619args = append(args, "run")620}621args = append(args, launchCtx.projectContextDir)622623cmd := remoteDevServerCmd(args, launchCtx)624cmd.Env = append(cmd.Env, "JETBRAINS_GITPOD_BACKEND_KIND="+launchCtx.alias)625workspaceUrl, err := url.Parse(launchCtx.wsInfo.WorkspaceUrl)626if err == nil {627cmd.Env = append(cmd.Env, "JETBRAINS_GITPOD_WORKSPACE_HOST="+workspaceUrl.Hostname())628}629// Enable host status endpoint630cmd.Env = append(cmd.Env, "CWM_HOST_STATUS_OVER_HTTP_TOKEN=gitpod")631632if err := cmd.Start(); err != nil {633log.WithError(err).Error("failed to start")634}635636// Nicely handle SIGTERM sinal637go handleSignal()638639if err := cmd.Wait(); err != nil {640log.WithError(err).Error("failed to wait")641}642log.Info("IDE stopped, exiting")643os.Exit(cmd.ProcessState.ExitCode())644}645646// resolveUserEnvs emulats the interactive login shell to ensure that all user defined shell scripts are loaded647func resolveUserEnvs() (userEnvs []string, err error) {648shell := os.Getenv("SHELL")649if shell == "" {650shell = "/bin/bash"651}652mark, err := uuid.NewRandom()653if err != nil {654return655}656657self, err := os.Executable()658if err != nil {659return660}661envCmd := exec.Command(shell, []string{"-i", "-l", "-c", strings.Join([]string{self, "env", mark.String()}, " ")}...)662envCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}663envCmd.Stderr = os.Stderr664envCmd.WaitDelay = 3 * time.Second665time.AfterFunc(8*time.Second, func() {666_ = syscall.Kill(-envCmd.Process.Pid, syscall.SIGKILL)667})668669output, err := envCmd.Output()670if errors.Is(err, exec.ErrWaitDelay) {671// For some reason the command doesn't close it's I/O pipes but it already run successfully672// so just ignore this error673log.Warn("WaitDelay expired before envCmd I/O completed")674} else if err != nil {675return676}677678markByte := []byte(mark.String())679start := bytes.Index(output, markByte)680if start == -1 {681err = fmt.Errorf("no %s in output", mark.String())682return683}684start = start + len(markByte)685if start > len(output) {686err = fmt.Errorf("no %s in output", mark.String())687return688}689end := bytes.LastIndex(output, markByte)690if end == -1 {691err = fmt.Errorf("no %s in output", mark.String())692return693}694err = json.Unmarshal(output[start:end], &userEnvs)695return696}697698func resolveLaunchContextEnv() []string {699var launchCtxEnv []string700userEnvs, err := resolveUserEnvs()701if err == nil {702launchCtxEnv = append(launchCtxEnv, userEnvs...)703} else {704log.WithError(err).Error("failed to resolve user env vars")705launchCtxEnv = os.Environ()706}707708// instead put them into /ide-desktop/${alias}${qualifier}/backend/bin/idea64.vmoptions709// otherwise JB will complain to a user on each startup710// by default remote dev already set -Xmx2048m, see /ide-desktop/${alias}${qualifier}/backend/plugins/remote-dev-server/bin/launcher.sh711launchCtxEnv = append(launchCtxEnv, "INTELLIJ_ORIGINAL_ENV_JAVA_TOOL_OPTIONS="+os.Getenv("JAVA_TOOL_OPTIONS"))712launchCtxEnv = append(launchCtxEnv, "JAVA_TOOL_OPTIONS=")713714// Force it to be disabled as we update platform properties file already715// TODO: Some ides have it enabled by default still, check pycharm and remove next release716launchCtxEnv = append(launchCtxEnv, "REMOTE_DEV_LEGACY_PER_PROJECT_CONFIGS=0")717718log.WithField("env", strings.Join(launchCtxEnv, "\n")).Info("resolved launch env")719720return launchCtxEnv721}722723func remoteDevServerCmd(args []string, launchCtx *LaunchContext) *exec.Cmd {724cmd := exec.Command(launchCtx.backendDir+"/bin/remote-dev-server.sh", args...)725cmd.Env = launchCtx.env726cmd.Stderr = os.Stderr727cmd.Stdout = os.Stdout728return cmd729}730731func handleSignal() {732sigChan := make(chan os.Signal, 1)733signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)734735<-sigChan736log.WithField("port", defaultBackendPort).Info("receive SIGTERM signal, terminating IDE")737if err := terminateIDE(defaultBackendPort); err != nil {738log.WithError(err).Error("failed to terminate IDE")739}740log.Info("asked IDE to terminate")741}742743func configurePlatformProperties(platformOptionsPath string, configDir string, systemDir string) (bool, error) {744buffer, err := os.ReadFile(platformOptionsPath)745if err != nil {746return false, err747}748749content := string(buffer)750751updated, content := updatePlatformProperties(content, configDir, systemDir)752753if updated {754return true, os.WriteFile(platformOptionsPath, []byte(content), 0)755}756757return false, nil758}759760func updatePlatformProperties(content string, configDir string, systemDir string) (bool, string) {761lines := strings.Split(content, "\n")762configMap := make(map[string]bool)763for _, v := range lines {764v = strings.TrimSpace(v)765if v != "" && !strings.HasPrefix(v, "#") {766key, _, found := strings.Cut(v, "=")767if found {768configMap[key] = true769}770}771}772773updated := false774775if _, found := configMap["idea.config.path"]; !found {776updated = true777content = strings.Join([]string{778content,779fmt.Sprintf("idea.config.path=%s", configDir),780fmt.Sprintf("idea.plugins.path=%s", configDir+"/plugins"),781fmt.Sprintf("idea.system.path=%s", systemDir),782fmt.Sprintf("idea.log.path=%s", systemDir+"/log"),783}, "\n")784}785786return updated, content787}788789func configureVMOptions(config *gitpod.GitpodConfig, alias string, vmOptionsPath string) error {790options, err := readVMOptions(vmOptionsPath)791if err != nil {792return err793}794newOptions := updateVMOptions(config, alias, options)795return writeVMOptions(vmOptionsPath, newOptions)796}797798func readVMOptions(vmOptionsPath string) ([]string, error) {799content, err := os.ReadFile(vmOptionsPath)800if err != nil {801return nil, err802}803return strings.Fields(string(content)), nil804}805806func writeVMOptions(vmOptionsPath string, vmoptions []string) error {807// vmoptions file should end with a newline808content := strings.Join(vmoptions, "\n") + "\n"809return os.WriteFile(vmOptionsPath, []byte(content), 0)810}811812// deduplicateVMOption append new VMOptions onto old VMOptions and remove any duplicated leftmost options813func deduplicateVMOption(oldLines []string, newLines []string, predicate func(l, r string) bool) []string {814var result []string815var merged = append(oldLines, newLines...)816for i, left := range merged {817for _, right := range merged[i+1:] {818if predicate(left, right) {819left = ""820break821}822}823if left != "" {824result = append(result, left)825}826}827return result828}829830func updateVMOptions(831config *gitpod.GitpodConfig,832alias string,833// original vmoptions (inherited from $JETBRAINS_IDE_HOME/bin/idea64.vmoptions)834ideaVMOptionsLines []string) []string {835// inspired by how intellij platform merge the VMOptions836// https://github.com/JetBrains/intellij-community/blob/master/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java#L1115837filterFunc := func(l, r string) bool {838isEqual := l == r839isXmx := strings.HasPrefix(l, "-Xmx") && strings.HasPrefix(r, "-Xmx")840isXms := strings.HasPrefix(l, "-Xms") && strings.HasPrefix(r, "-Xms")841isXss := strings.HasPrefix(l, "-Xss") && strings.HasPrefix(r, "-Xss")842isXXOptions := strings.HasPrefix(l, "-XX:") && strings.HasPrefix(r, "-XX:") &&843strings.Split(l, "=")[0] == strings.Split(r, "=")[0]844return isEqual || isXmx || isXms || isXss || isXXOptions845}846// Gitpod's default customization847var gitpodVMOptions []string848gitpodVMOptions = append(gitpodVMOptions, "-Dgtw.disable.exit.dialog=true")849// temporary disable auto-attach of the async-profiler to prevent JVM crash850// 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.io851gitpodVMOptions = append(gitpodVMOptions, "-Dfreeze.reporter.profiling=false")852if alias == "intellij" {853gitpodVMOptions = append(gitpodVMOptions, "-Djdk.configure.existing=true")854}855// container relevant options856gitpodVMOptions = append(gitpodVMOptions, "-XX:+UseContainerSupport")857cpuCount := os.Getenv("GITPOD_CPU_COUNT")858parsedCPUCount, err := strconv.Atoi(cpuCount)859// if CPU count is set and is parseable as a positive number860if err == nil && parsedCPUCount > 0 && parsedCPUCount <= 16 {861gitpodVMOptions = append(gitpodVMOptions, "-XX:ActiveProcessorCount="+cpuCount)862}863864memory := os.Getenv("GITPOD_MEMORY")865parsedMemory, err := strconv.Atoi(memory)866if err == nil && parsedMemory > 0 {867xmx := (float64(parsedMemory) * 0.6)868if xmx > maxDefaultXmx { // 8G869xmx = maxDefaultXmx870}871if xmx > minDefaultXmx {872gitpodVMOptions = append(gitpodVMOptions, fmt.Sprintf("-Xmx%dm", int(xmx)))873}874}875876vmoptions := deduplicateVMOption(ideaVMOptionsLines, gitpodVMOptions, filterFunc)877878// user-defined vmoptions (EnvVar)879userVMOptionsVar := os.Getenv(strings.ToUpper(alias) + "_VMOPTIONS")880userVMOptions := strings.Fields(userVMOptionsVar)881if len(userVMOptions) > 0 {882vmoptions = deduplicateVMOption(vmoptions, userVMOptions, filterFunc)883}884885// project-defined vmoptions (.gitpod.yml)886if config != nil {887productConfig := getProductConfig(config, alias)888if productConfig != nil {889projectVMOptions := strings.Fields(productConfig.Vmoptions)890if len(projectVMOptions) > 0 {891vmoptions = deduplicateVMOption(vmoptions, projectVMOptions, filterFunc)892}893}894}895896return vmoptions897}898899/*900*901902{903"buildNumber" : "221.4994.44",904"customProperties" : [ ],905"dataDirectoryName" : "IntelliJIdea2022.1",906"launch" : [ {907"javaExecutablePath" : "jbr/bin/java",908"launcherPath" : "bin/idea.sh",909"os" : "Linux",910"startupWmClass" : "jetbrains-idea",911"vmOptionsFilePath" : "bin/idea64.vmoptions"912} ],913"name" : "IntelliJ IDEA",914"productCode" : "IU",915"svgIconPath" : "bin/idea.svg",916"version" : "2022.1",917"versionSuffix" : "EAP"918}919*/920type ProductInfo struct {921BuildNumber string `json:"buildNumber"`922Version string `json:"version"`923ProductCode string `json:"productCode"`924}925926func resolveProductInfo(backendDir string) (*ProductInfo, error) {927f, err := os.Open(backendDir + "/product-info.json")928if err != nil {929return nil, err930}931defer f.Close()932content, err := ioutil.ReadAll(f)933if err != nil {934return nil, err935}936937var info ProductInfo938err = json.Unmarshal(content, &info)939return &info, err940}941942type SyncTarget string943944const (945Options SyncTarget = "options"946Plugins SyncTarget = "plugins"947)948949func syncInitialContent(launchCtx *LaunchContext, target SyncTarget) (bool, error) {950destDir, err, alreadySynced := ensureInitialSyncDest(launchCtx, target)951if alreadySynced {952log.Infof("initial %s is already synced, skipping", target)953return alreadySynced, nil954}955if err != nil {956return alreadySynced, err957}958959srcDirs, err := collectSyncSources(launchCtx, target)960if err != nil {961return alreadySynced, err962}963if len(srcDirs) == 0 {964// nothing to sync965return alreadySynced, nil966}967968for _, srcDir := range srcDirs {969if target == Plugins {970files, err := ioutil.ReadDir(srcDir)971if err != nil {972return alreadySynced, err973}974975for _, file := range files {976err := syncPlugin(file, srcDir, destDir)977if err != nil {978log.WithError(err).WithField("file", file.Name()).WithField("srcDir", srcDir).WithField("destDir", destDir).Error("failed to sync plugin")979}980}981} else {982cp := exec.Command("cp", "-rf", srcDir+"/.", destDir)983err = cp.Run()984if err != nil {985return alreadySynced, err986}987}988}989return alreadySynced, nil990}991992func syncPlugin(file fs.FileInfo, srcDir, destDir string) error {993if file.IsDir() {994_, err := os.Stat(filepath.Join(destDir, file.Name()))995if !os.IsNotExist(err) {996log.WithField("plugin", file.Name()).Info("plugin is already synced, skipping")997return nil998}999return exec.Command("cp", "-rf", filepath.Join(srcDir, file.Name()), destDir).Run()1000}1001if filepath.Ext(file.Name()) != ".zip" {1002return nil1003}1004archiveFile := filepath.Join(srcDir, file.Name())1005rootDir, err := getRootDirFromArchive(archiveFile)1006if err != nil {1007return err1008}1009_, err = os.Stat(filepath.Join(destDir, rootDir))1010if !os.IsNotExist(err) {1011log.WithField("plugin", rootDir).Info("plugin is already synced, skipping")1012return nil1013}1014return unzipArchive(archiveFile, destDir)1015}10161017func ensureInitialSyncDest(launchCtx *LaunchContext, target SyncTarget) (string, error, bool) {1018targetDestDir := launchCtx.configDir1019if target == Plugins {1020targetDestDir = launchCtx.backendDir1021}1022destDir := fmt.Sprintf("%s/%s", targetDestDir, target)1023if target == Options {1024_, err := os.Stat(destDir)1025if !os.IsNotExist(err) {1026return "", nil, true1027}1028err = os.MkdirAll(destDir, os.ModePerm)1029if err != nil {1030return "", err, false1031}1032}1033return destDir, nil, false1034}10351036func collectSyncSources(launchCtx *LaunchContext, target SyncTarget) ([]string, error) {1037userHomeDir, err := os.UserHomeDir()1038if err != nil {1039return nil, err1040}1041var srcDirs []string1042for _, srcDir := range []string{1043fmt.Sprintf("%s/.gitpod/jetbrains/%s", userHomeDir, target),1044fmt.Sprintf("%s/.gitpod/jetbrains/%s/%s", userHomeDir, launchCtx.alias, target),1045fmt.Sprintf("%s/.gitpod/jetbrains/%s", launchCtx.projectDir, target),1046fmt.Sprintf("%s/.gitpod/jetbrains/%s/%s", launchCtx.projectDir, launchCtx.alias, target),1047} {1048srcStat, err := os.Stat(srcDir)1049if os.IsNotExist(err) {1050// nothing to sync1051continue1052}1053if err != nil {1054return nil, err1055}1056if !srcStat.IsDir() {1057return nil, fmt.Errorf("%s is not a directory", srcDir)1058}1059srcDirs = append(srcDirs, srcDir)1060}1061return srcDirs, nil1062}10631064func getRootDirFromArchive(zipPath string) (string, error) {1065r, err := zip.OpenReader(zipPath)1066if err != nil {1067return "", err1068}1069defer r.Close()10701071if len(r.File) == 0 {1072return "", fmt.Errorf("empty archive")1073}10741075// Assuming the first file in the zip is the root directory or a file in the root directory1076return strings.SplitN(r.File[0].Name, "/", 2)[0], nil1077}10781079func unzipArchive(src, dest string) error {1080r, err := zip.OpenReader(src)1081if err != nil {1082return err1083}1084defer r.Close()10851086for _, f := range r.File {1087rc, err := f.Open()1088if err != nil {1089return err1090}1091defer rc.Close()10921093fpath := filepath.Join(dest, f.Name)1094if f.FileInfo().IsDir() {1095err := os.MkdirAll(fpath, os.ModePerm)1096if err != nil {1097return err1098}1099} else {1100fdir := filepath.Dir(fpath)1101err := os.MkdirAll(fdir, os.ModePerm)1102if err != nil {1103return err1104}11051106outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())1107if err != nil {1108return err1109}1110defer outFile.Close()11111112_, err = io.Copy(outFile, rc)1113if err != nil {1114return err1115}1116}1117}1118return nil1119}11201121func installPlugins(config *gitpod.GitpodConfig, launchCtx *LaunchContext) error {1122plugins, err := getPlugins(config, launchCtx.alias)1123if err != nil {1124return err1125}1126if len(plugins) <= 0 {1127return nil1128}11291130var args []string1131args = append(args, "installPlugins")1132args = append(args, launchCtx.projectContextDir)1133args = append(args, plugins...)1134cmd := remoteDevServerCmd(args, launchCtx)1135installErr := cmd.Run()11361137// delete alien_plugins.txt to suppress 3rd-party plugins consent on startup to workaround backend startup freeze1138err = os.Remove(launchCtx.configDir + "/alien_plugins.txt")1139if err != nil && !os.IsNotExist(err) && !strings.Contains(err.Error(), "no such file or directory") {1140log.WithError(err).Error("failed to suppress 3rd-party plugins consent")1141}11421143if installErr != nil {1144return errors.New("failed to install repo plugins: " + installErr.Error())1145}1146return nil1147}11481149func parseGitpodConfig(repoRoot string) (*gitpod.GitpodConfig, error) {1150if repoRoot == "" {1151return nil, errors.New("repoRoot is empty")1152}1153data, err := os.ReadFile(filepath.Join(repoRoot, ".gitpod.yml"))1154if err != nil {1155// .gitpod.yml not exist is ok1156if errors.Is(err, os.ErrNotExist) {1157return nil, nil1158}1159return nil, errors.New("read .gitpod.yml file failed: " + err.Error())1160}1161var config *gitpod.GitpodConfig1162if err = yaml.Unmarshal(data, &config); err != nil {1163return nil, errors.New("unmarshal .gitpod.yml file failed" + err.Error())1164}1165return config, nil1166}11671168func getPlugins(config *gitpod.GitpodConfig, alias string) ([]string, error) {1169var plugins []string1170if config == nil || config.Jetbrains == nil {1171return nil, nil1172}1173if config.Jetbrains.Plugins != nil {1174plugins = append(plugins, config.Jetbrains.Plugins...)1175}1176productConfig := getProductConfig(config, alias)1177if productConfig != nil && productConfig.Plugins != nil {1178plugins = append(plugins, productConfig.Plugins...)1179}1180return plugins, nil1181}11821183func getProductConfig(config *gitpod.GitpodConfig, alias string) *gitpod.JetbrainsProduct {1184defer func() {1185if err := recover(); err != nil {1186log.WithField("error", err).WithField("alias", alias).Error("failed to extract JB product config")1187}1188}()1189v := reflect.ValueOf(*config.Jetbrains).FieldByNameFunc(func(s string) bool {1190return strings.ToLower(s) == alias1191}).Interface()1192productConfig, ok := v.(*gitpod.JetbrainsProduct)1193if !ok {1194return nil1195}1196return productConfig1197}11981199func linkRemotePlugin(launchCtx *LaunchContext) error {1200remotePluginsFolder := launchCtx.configDir + "/plugins"1201if launchCtx.info.Version == "2022.3.3" {1202remotePluginsFolder = launchCtx.backendDir + "/plugins"1203}1204remotePluginDir := remotePluginsFolder + "/gitpod-remote"1205if err := os.MkdirAll(remotePluginsFolder, 0755); err != nil {1206return err1207}12081209// added for backwards compatibility, can be removed in the future1210sourceDir := "/ide-desktop-plugins/gitpod-remote-" + os.Getenv("JETBRAINS_BACKEND_QUALIFIER")1211_, err := os.Stat(sourceDir)1212if err == nil {1213return safeLink(sourceDir, remotePluginDir)1214}12151216return safeLink("/ide-desktop-plugins/gitpod-remote", remotePluginDir)1217}12181219// safeLink creates a symlink from source to target, removing the old target if it exists1220func safeLink(source, target string) error {1221if _, err := os.Lstat(target); err == nil {1222// unlink the old symlink1223if err2 := os.RemoveAll(target); err2 != nil {1224log.WithError(err).Error("failed to unlink old symlink")1225}1226}1227return os.Symlink(source, target)1228}12291230// TODO(andreafalzetti): remove dir scanning once this is implemented https://youtrack.jetbrains.com/issue/GTW-2402/Rider-Open-Project-dialog-not-displaying-in-remote-dev1231func findRiderSolutionFile(root string) (string, error) {1232slnRegEx := regexp.MustCompile(`^.+\.sln$`)1233projRegEx := regexp.MustCompile(`^.+\.csproj$`)12341235var slnFiles []string1236var csprojFiles []string12371238err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {1239if err != nil {1240return err1241} else if slnRegEx.MatchString(info.Name()) {1242slnFiles = append(slnFiles, path)1243} else if projRegEx.MatchString(info.Name()) {1244csprojFiles = append(csprojFiles, path)1245}1246return nil1247})12481249if err != nil {1250return "", err1251}12521253if len(slnFiles) > 0 {1254return slnFiles[0], nil1255} else if len(csprojFiles) > 0 {1256return csprojFiles[0], nil1257}12581259return root, nil1260}12611262func resolveProjectContextDir(launchCtx *LaunchContext) string {1263if launchCtx.alias == "rider" {1264return launchCtx.riderSolutionFile1265}12661267return launchCtx.projectDir1268}12691270func configureToolboxCliProperties(backendDir string) error {1271userHomeDir, err := os.UserHomeDir()1272if err != nil {1273return err1274}12751276toolboxCliPropertiesDir := fmt.Sprintf("%s/.local/share/JetBrains/Toolbox", userHomeDir)1277_, err = os.Stat(toolboxCliPropertiesDir)1278if !os.IsNotExist(err) {1279return err1280}1281err = os.MkdirAll(toolboxCliPropertiesDir, os.ModePerm)1282if err != nil {1283return err1284}12851286toolboxCliPropertiesFilePath := fmt.Sprintf("%s/environment.json", toolboxCliPropertiesDir)12871288debuggingToolbox := os.Getenv("GITPOD_TOOLBOX_DEBUGGING")1289allowInstallation := strconv.FormatBool(strings.Contains(debuggingToolbox, "allowInstallation"))12901291// TODO(hw): restrict IDE installation1292content := fmt.Sprintf(`{1293"tools": {1294"allowInstallation": %s,1295"allowUpdate": false,1296"allowUninstallation": %s,1297"location": [1298{1299"path": "%s"1300}1301]1302}1303}`, allowInstallation, allowInstallation, backendDir)13041305return os.WriteFile(toolboxCliPropertiesFilePath, []byte(content), 0o644)1306}130713081309