// Copyright (c) 2022 Gitpod GmbH. All rights reserved. // Licensed under the GNU Affero General Public License (AGPL). // See License.AGPL.txt in the project root for license information. package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/ioutil" "net" "net/http" "net/url" "os" "os/exec" "os/signal" "path/filepath" "reflect" "regexp" "strconv" "strings" "syscall" "time" "github.com/google/uuid" "github.com/hashicorp/go-version" "golang.org/x/xerrors" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" yaml "gopkg.in/yaml.v2" "github.com/gitpod-io/gitpod/common-go/log" "github.com/gitpod-io/gitpod/common-go/util" gitpod "github.com/gitpod-io/gitpod/gitpod-protocol" supervisor "github.com/gitpod-io/gitpod/supervisor/api" ) const defaultBackendPort = "63342" var ( // ServiceName is the name we use for tracing/logging. ServiceName = "jetbrains-launcher" // Version of this service - set during build. Version = "" ) type LaunchContext struct { startTime time.Time port string alias string label string warmup bool qualifier string productDir string backendDir string info *ProductInfo backendVersion *version.Version wsInfo *supervisor.WorkspaceInfoResponse vmOptionsFile string projectDir string configDir string systemDir string projectConfigDir string projectContextDir string riderSolutionFile string env []string } // JB startup entrypoint func main() { log.Init(ServiceName, Version, true, false) if len(os.Args) == 3 && os.Args[1] == "env" && os.Args[2] != "" { var mark = os.Args[2] content, err := json.Marshal(os.Environ()) if err != nil { log.WithError(err).Fatal() } fmt.Printf("%s%s%s", mark, content, mark) return } log.Info(ServiceName + ": " + Version) startTime := time.Now() var port string var warmup bool if len(os.Args) < 2 { log.Fatalf("Usage: %s (warmup|)\n", os.Args[0]) } if os.Args[1] == "warmup" { if len(os.Args) < 3 { log.Fatalf("Usage: %s %s \n", os.Args[0], os.Args[1]) } warmup = true } else { if len(os.Args) < 3 { log.Fatalf("Usage: %s []\n", os.Args[0]) } port = os.Args[1] } alias := os.Args[2] label := "Open JetBrains IDE" if len(os.Args) > 3 { label = os.Args[3] } qualifier := os.Getenv("JETBRAINS_BACKEND_QUALIFIER") if qualifier == "stable" { qualifier = "" } else { qualifier = "-" + qualifier } productDir := "/ide-desktop/" + alias + qualifier backendDir := productDir + "/backend" info, err := resolveProductInfo(backendDir) if err != nil { log.WithError(err).Error("failed to resolve product info") return } backendVersion, err := version.NewVersion(info.Version) if err != nil { log.WithError(err).Error("failed to resolve backend version") return } wsInfo, err := resolveWorkspaceInfo(context.Background()) if err != nil || wsInfo == nil { log.WithError(err).WithField("wsInfo", wsInfo).Error("resolve workspace info failed") return } launchCtx := &LaunchContext{ startTime: startTime, warmup: warmup, port: port, alias: alias, label: label, qualifier: qualifier, productDir: productDir, backendDir: backendDir, info: info, backendVersion: backendVersion, wsInfo: wsInfo, } if launchCtx.warmup { waitForTasksToFinish() launch(launchCtx) return } // we should start serving immediately and postpone launch // in order to enable a JB Gateway to connect as soon as possible go launch(launchCtx) // IMPORTANT: don't put startup logic in serve!!! serve(launchCtx) } func serve(launchCtx *LaunchContext) { debugAgentPrefix := "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" http.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) { options, err := readVMOptions(launchCtx.vmOptionsFile) if err != nil { log.WithError(err).Error("failed to configure debug agent") http.Error(w, err.Error(), http.StatusInternalServerError) return } debugPort := "" i := len(options) - 1 for i >= 0 && debugPort == "" { option := options[i] if strings.HasPrefix(option, debugAgentPrefix) { debugPort = option[len(debugAgentPrefix):] if debugPort == "0" { debugPort = "" } } i-- } if debugPort != "" { fmt.Fprint(w, debugPort) return } netListener, err := net.Listen("tcp", "localhost:0") if err != nil { log.WithError(err).Error("failed to configure debug agent") http.Error(w, err.Error(), http.StatusInternalServerError) return } debugPort = strconv.Itoa(netListener.(*net.TCPListener).Addr().(*net.TCPAddr).Port) _ = netListener.Close() debugOptions := []string{debugAgentPrefix + debugPort} options = deduplicateVMOption(options, debugOptions, func(l, r string) bool { return strings.HasPrefix(l, debugAgentPrefix) && strings.HasPrefix(r, debugAgentPrefix) }) err = writeVMOptions(launchCtx.vmOptionsFile, options) if err != nil { log.WithError(err).Error("failed to configure debug agent") http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprint(w, debugPort) restart(r) }) http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "terminated") restart(r) }) http.HandleFunc("/joinLink", func(w http.ResponseWriter, r *http.Request) { backendPort := r.URL.Query().Get("backendPort") if backendPort == "" { backendPort = defaultBackendPort } jsonLink, err := resolveJsonLink(backendPort) if err != nil { log.WithError(err).Error("cannot resolve join link") http.Error(w, err.Error(), http.StatusServiceUnavailable) return } fmt.Fprint(w, jsonLink) }) http.HandleFunc("/gatewayLink", func(w http.ResponseWriter, r *http.Request) { backendPort := r.URL.Query().Get("backendPort") if backendPort == "" { backendPort = defaultBackendPort } jsonLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo) if err != nil { log.WithError(err).Error("cannot resolve gateway link") http.Error(w, err.Error(), http.StatusServiceUnavailable) return } fmt.Fprint(w, jsonLink) }) http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { backendPort := r.URL.Query().Get("backendPort") if backendPort == "" { backendPort = defaultBackendPort } gatewayLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo) if err != nil { log.WithError(err).Error("cannot resolve gateway link") http.Error(w, err.Error(), http.StatusServiceUnavailable) return } response := make(map[string]string) response["link"] = gatewayLink response["label"] = launchCtx.label response["clientID"] = "jetbrains-gateway" response["kind"] = launchCtx.alias w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response) }) fmt.Printf("Starting status proxy for desktop IDE at port %s\n", launchCtx.port) if err := http.ListenAndServe(fmt.Sprintf(":%s", launchCtx.port), nil); err != nil { log.Fatal(err) } } func restart(r *http.Request) { backendPort := r.URL.Query().Get("backendPort") if backendPort == "" { backendPort = defaultBackendPort } err := terminateIDE(backendPort) if err != nil { log.WithError(err).Error("failed to terminate IDE gracefully") os.Exit(1) } os.Exit(0) } type Projects struct { JoinLink string `json:"joinLink"` } type Response struct { Projects []Projects `json:"projects"` } func resolveGatewayLink(backendPort string, wsInfo *supervisor.WorkspaceInfoResponse) (string, error) { gitpodUrl, err := url.Parse(wsInfo.GitpodHost) if err != nil { return "", err } debugWorkspace := wsInfo.DebugWorkspaceType != supervisor.DebugWorkspaceType_noDebug link := url.URL{ Scheme: "jetbrains-gateway", Host: "connect", Fragment: fmt.Sprintf("gitpodHost=%s&workspaceId=%s&backendPort=%s&debugWorkspace=%t", gitpodUrl.Hostname(), wsInfo.WorkspaceId, backendPort, debugWorkspace), } return link.String(), nil } func resolveJsonLink(backendPort string) (string, error) { var ( hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod" client = http.Client{Timeout: 1 * time.Second} ) resp, err := client.Get(hostStatusUrl) if err != nil { return "", err } defer resp.Body.Close() bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } if resp.StatusCode != http.StatusOK { return "", xerrors.Errorf("failed to resolve project status: %s (%d)", bodyBytes, resp.StatusCode) } jsonResp := &Response{} err = json.Unmarshal(bodyBytes, &jsonResp) if err != nil { return "", err } if len(jsonResp.Projects) != 1 { return "", xerrors.Errorf("project is not found") } return jsonResp.Projects[0].JoinLink, nil } func terminateIDE(backendPort string) error { var ( hostStatusUrl = "http://localhost:" + backendPort + "/codeWithMe/unattendedHostStatus?token=gitpod&exit=true" client = http.Client{Timeout: 10 * time.Second} ) resp, err := client.Get(hostStatusUrl) if err != nil { return err } defer resp.Body.Close() bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode != http.StatusOK { return xerrors.Errorf("failed to resolve terminate IDE: %s (%d)", bodyBytes, resp.StatusCode) } return nil } func resolveWorkspaceInfo(ctx context.Context) (*supervisor.WorkspaceInfoResponse, error) { resolve := func(ctx context.Context) (wsInfo *supervisor.WorkspaceInfoResponse, err error) { supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { err = errors.New("dial supervisor failed: " + err.Error()) return } defer supervisorConn.Close() if wsInfo, err = supervisor.NewInfoServiceClient(supervisorConn).WorkspaceInfo(ctx, &supervisor.WorkspaceInfoRequest{}); err != nil { err = errors.New("get workspace info failed: " + err.Error()) return } return } // try resolve workspace info 10 times for attempt := 0; attempt < 10; attempt++ { if wsInfo, err := resolve(ctx); err != nil { log.WithError(err).Error("resolve workspace info failed") time.Sleep(1 * time.Second) } else { return wsInfo, err } } return nil, errors.New("failed with attempt 10 times") } func launch(launchCtx *LaunchContext) { projectDir := launchCtx.wsInfo.GetCheckoutLocation() gitpodConfig, err := parseGitpodConfig(projectDir) if err != nil { log.WithError(err).Error("failed to parse .gitpod.yml") } // configure vmoptions idePrefix := launchCtx.alias if launchCtx.alias == "intellij" { idePrefix = "idea" } // [idea64|goland64|pycharm64|phpstorm64].vmoptions launchCtx.vmOptionsFile = fmt.Sprintf(launchCtx.backendDir+"/bin/%s64.vmoptions", idePrefix) err = configureVMOptions(gitpodConfig, launchCtx.alias, launchCtx.vmOptionsFile) if err != nil { log.WithError(err).Error("failed to configure vmoptions") } var riderSolutionFile string if launchCtx.alias == "rider" { riderSolutionFile, err = findRiderSolutionFile(projectDir) if err != nil { log.WithError(err).Error("failed to find a rider solution file") } } configDir := fmt.Sprintf("/workspace/.config/JetBrains%s", launchCtx.qualifier) launchCtx.projectDir = projectDir launchCtx.configDir = configDir launchCtx.systemDir = fmt.Sprintf("/workspace/.cache/JetBrains%s", launchCtx.qualifier) launchCtx.riderSolutionFile = riderSolutionFile launchCtx.projectContextDir = resolveProjectContextDir(launchCtx) launchCtx.projectConfigDir = fmt.Sprintf("%s/RemoteDev-%s/%s", configDir, launchCtx.info.ProductCode, strings.ReplaceAll(launchCtx.projectContextDir, "/", "_")) // sync initial options err = syncOptions(launchCtx) if err != nil { log.WithError(err).Error("failed to sync initial options") } // install project plugins version_2022_1, _ := version.NewVersion("2022.1") if version_2022_1.LessThanOrEqual(launchCtx.backendVersion) { err = installPlugins(gitpodConfig, launchCtx) installPluginsCost := time.Now().Local().Sub(launchCtx.startTime).Milliseconds() if err != nil { log.WithError(err).WithField("cost", installPluginsCost).Error("installing repo plugins: done") } else { log.WithField("cost", installPluginsCost).Info("installing repo plugins: done") } } // install gitpod plugin err = linkRemotePlugin(launchCtx) if err != nil { log.WithError(err).Error("failed to install gitpod-remote plugin") } // run backend run(launchCtx) } func run(launchCtx *LaunchContext) { var args []string if launchCtx.warmup { args = append(args, "warmup") } else { args = append(args, "run") } args = append(args, launchCtx.projectContextDir) cmd := remoteDevServerCmd(args, launchCtx) cmd.Env = append(cmd.Env, "JETBRAINS_GITPOD_BACKEND_KIND="+launchCtx.alias) workspaceUrl, err := url.Parse(launchCtx.wsInfo.WorkspaceUrl) if err == nil { cmd.Env = append(cmd.Env, "JETBRAINS_GITPOD_WORKSPACE_HOST="+workspaceUrl.Hostname()) } // Enable host status endpoint cmd.Env = append(cmd.Env, "CWM_HOST_STATUS_OVER_HTTP_TOKEN=gitpod") if err := cmd.Start(); err != nil { log.WithError(err).Error("failed to start") } // Nicely handle SIGTERM sinal go handleSignal() if err := cmd.Wait(); err != nil { log.WithError(err).Error("failed to wait") } log.Info("IDE stopped, exiting") os.Exit(cmd.ProcessState.ExitCode()) } // resolveUserEnvs emulats the interactive login shell to ensure that all user defined shell scripts are loaded func resolveUserEnvs(launchCtx *LaunchContext) (userEnvs []string, err error) { shell := os.Getenv("SHELL") if shell == "" { shell = "/bin/bash" } mark, err := uuid.NewRandom() if err != nil { return } envCmd := exec.Command(shell, []string{"-ilc", "/ide-desktop/jb-launcher env " + mark.String()}...) envCmd.Stderr = os.Stderr output, err := envCmd.Output() if err != nil { return } markByte := []byte(mark.String()) start := bytes.Index(output, markByte) if start == -1 { err = fmt.Errorf("no %s in output", mark.String()) return } start = start + len(markByte) if start > len(output) { err = fmt.Errorf("no %s in output", mark.String()) return } end := bytes.LastIndex(output, markByte) if end == -1 { err = fmt.Errorf("no %s in output", mark.String()) return } err = json.Unmarshal(output[start:end], &userEnvs) return } func remoteDevServerCmd(args []string, launchCtx *LaunchContext) *exec.Cmd { if launchCtx.env == nil { userEnvs, err := resolveUserEnvs(launchCtx) if err == nil { launchCtx.env = append(launchCtx.env, userEnvs...) } else { log.WithError(err).Error("failed to resolve user env vars") launchCtx.env = os.Environ() } // Set default config and system directories under /workspace to preserve between restarts launchCtx.env = append(launchCtx.env, // Set default config and system directories under /workspace to preserve between restarts fmt.Sprintf("IJ_HOST_CONFIG_BASE_DIR=%s", launchCtx.configDir), fmt.Sprintf("IJ_HOST_SYSTEM_BASE_DIR=%s", launchCtx.systemDir), ) // instead put them into /ide-desktop/${alias}${qualifier}/backend/bin/idea64.vmoptions // otherwise JB will complain to a user on each startup // by default remote dev already set -Xmx2048m, see /ide-desktop/${alias}${qualifier}/backend/plugins/remote-dev-server/bin/launcher.sh launchCtx.env = append(launchCtx.env, "JAVA_TOOL_OPTIONS=") log.WithField("env", launchCtx.env).Debug("resolved launch env") } cmd := exec.Command(launchCtx.backendDir+"/bin/remote-dev-server.sh", args...) cmd.Env = launchCtx.env cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout return cmd } func handleSignal() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) <-sigChan log.WithField("port", defaultBackendPort).Info("receive SIGTERM signal, terminating IDE") if err := terminateIDE(defaultBackendPort); err != nil { log.WithError(err).Error("failed to terminate IDE") } log.Info("asked IDE to terminate") } func configureVMOptions(config *gitpod.GitpodConfig, alias string, vmOptionsPath string) error { options, err := readVMOptions(vmOptionsPath) if err != nil { return err } newOptions := updateVMOptions(config, alias, options) return writeVMOptions(vmOptionsPath, newOptions) } func readVMOptions(vmOptionsPath string) ([]string, error) { content, err := ioutil.ReadFile(vmOptionsPath) if err != nil { return nil, err } return strings.Fields(string(content)), nil } func writeVMOptions(vmOptionsPath string, vmoptions []string) error { // vmoptions file should end with a newline content := strings.Join(vmoptions, "\n") + "\n" return ioutil.WriteFile(vmOptionsPath, []byte(content), 0) } // deduplicateVMOption append new VMOptions onto old VMOptions and remove any duplicated leftmost options func deduplicateVMOption(oldLines []string, newLines []string, predicate func(l, r string) bool) []string { var result []string var merged = append(oldLines, newLines...) for i, left := range merged { for _, right := range merged[i+1:] { if predicate(left, right) { left = "" break } } if left != "" { result = append(result, left) } } return result } func updateVMOptions( config *gitpod.GitpodConfig, alias string, // original vmoptions (inherited from $JETBRAINS_IDE_HOME/bin/idea64.vmoptions) ideaVMOptionsLines []string) []string { // inspired by how intellij platform merge the VMOptions // https://github.com/JetBrains/intellij-community/blob/master/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java#L1115 filterFunc := func(l, r string) bool { isEqual := l == r isXmx := strings.HasPrefix(l, "-Xmx") && strings.HasPrefix(r, "-Xmx") isXms := strings.HasPrefix(l, "-Xms") && strings.HasPrefix(r, "-Xms") isXss := strings.HasPrefix(l, "-Xss") && strings.HasPrefix(r, "-Xss") isXXOptions := strings.HasPrefix(l, "-XX:") && strings.HasPrefix(r, "-XX:") && strings.Split(l, "=")[0] == strings.Split(r, "=")[0] return isEqual || isXmx || isXms || isXss || isXXOptions } // Gitpod's default customization var gitpodVMOptions []string gitpodVMOptions = append(gitpodVMOptions, "-Dgtw.disable.exit.dialog=true") if alias == "intellij" { gitpodVMOptions = append(gitpodVMOptions, "-Djdk.configure.existing=true") } vmoptions := deduplicateVMOption(ideaVMOptionsLines, gitpodVMOptions, filterFunc) // user-defined vmoptions (EnvVar) userVMOptionsVar := os.Getenv(strings.ToUpper(alias) + "_VMOPTIONS") userVMOptions := strings.Fields(userVMOptionsVar) if len(userVMOptions) > 0 { vmoptions = deduplicateVMOption(vmoptions, userVMOptions, filterFunc) } // project-defined vmoptions (.gitpod.yml) if config != nil { productConfig := getProductConfig(config, alias) if productConfig != nil { projectVMOptions := strings.Fields(productConfig.Vmoptions) if len(projectVMOptions) > 0 { vmoptions = deduplicateVMOption(vmoptions, projectVMOptions, filterFunc) } } } return vmoptions } /* * { "buildNumber" : "221.4994.44", "customProperties" : [ ], "dataDirectoryName" : "IntelliJIdea2022.1", "launch" : [ { "javaExecutablePath" : "jbr/bin/java", "launcherPath" : "bin/idea.sh", "os" : "Linux", "startupWmClass" : "jetbrains-idea", "vmOptionsFilePath" : "bin/idea64.vmoptions" } ], "name" : "IntelliJ IDEA", "productCode" : "IU", "svgIconPath" : "bin/idea.svg", "version" : "2022.1", "versionSuffix" : "EAP" } */ type ProductInfo struct { Version string `json:"version"` ProductCode string `json:"productCode"` } func resolveProductInfo(backendDir string) (*ProductInfo, error) { f, err := os.Open(backendDir + "/product-info.json") if err != nil { return nil, err } defer f.Close() content, err := ioutil.ReadAll(f) if err != nil { return nil, err } var info ProductInfo err = json.Unmarshal(content, &info) return &info, err } func syncOptions(launchCtx *LaunchContext) error { userHomeDir, err := os.UserHomeDir() if err != nil { return err } var srcDirs []string for _, srcDir := range []string{ fmt.Sprintf("%s/.gitpod/jetbrains/options", userHomeDir), fmt.Sprintf("%s/.gitpod/jetbrains/%s/options", userHomeDir, launchCtx.alias), fmt.Sprintf("%s/.gitpod/jetbrains/options", launchCtx.projectDir), fmt.Sprintf("%s/.gitpod/jetbrains/%s/options", launchCtx.projectDir, launchCtx.alias), } { srcStat, err := os.Stat(srcDir) if os.IsNotExist(err) { // nothing to sync continue } if err != nil { return err } if !srcStat.IsDir() { return fmt.Errorf("%s is not a directory", srcDir) } srcDirs = append(srcDirs, srcDir) } if len(srcDirs) == 0 { // nothing to sync return nil } destDir := fmt.Sprintf("%s/options", launchCtx.projectConfigDir) _, err = os.Stat(destDir) if !os.IsNotExist(err) { // already synced skipping, i.e. restart of jb backend return nil } err = os.MkdirAll(destDir, os.ModePerm) if err != nil { return err } for _, srcDir := range srcDirs { cp := exec.Command("cp", "-rf", srcDir+"/.", destDir) err = cp.Run() if err != nil { return err } } return nil } func installPlugins(config *gitpod.GitpodConfig, launchCtx *LaunchContext) error { plugins, err := getPlugins(config, launchCtx.alias) if err != nil { return err } if len(plugins) <= 0 { return nil } var args []string args = append(args, "installPlugins") args = append(args, launchCtx.projectContextDir) args = append(args, plugins...) cmd := remoteDevServerCmd(args, launchCtx) installErr := cmd.Run() // delete alien_plugins.txt to suppress 3rd-party plugins consent on startup to workaround backend startup freeze err = os.Remove(launchCtx.projectConfigDir + "/alien_plugins.txt") if err != nil { log.WithError(err).Error("failed to suppress 3rd-party plugins consent") } if installErr != nil { return errors.New("failed to install repo plugins: " + installErr.Error()) } return nil } func parseGitpodConfig(repoRoot string) (*gitpod.GitpodConfig, error) { if repoRoot == "" { return nil, errors.New("repoRoot is empty") } data, err := os.ReadFile(filepath.Join(repoRoot, ".gitpod.yml")) if err != nil { // .gitpod.yml not exist is ok if errors.Is(err, os.ErrNotExist) { return nil, nil } return nil, errors.New("read .gitpod.yml file failed: " + err.Error()) } var config *gitpod.GitpodConfig if err = yaml.Unmarshal(data, &config); err != nil { return nil, errors.New("unmarshal .gitpod.yml file failed" + err.Error()) } return config, nil } func getPlugins(config *gitpod.GitpodConfig, alias string) ([]string, error) { var plugins []string if config == nil || config.Jetbrains == nil { return nil, nil } if config.Jetbrains.Plugins != nil { plugins = append(plugins, config.Jetbrains.Plugins...) } productConfig := getProductConfig(config, alias) if productConfig != nil && productConfig.Plugins != nil { plugins = append(plugins, productConfig.Plugins...) } return plugins, nil } func getProductConfig(config *gitpod.GitpodConfig, alias string) *gitpod.JetbrainsProduct { defer func() { if err := recover(); err != nil { log.WithField("error", err).WithField("alias", alias).Error("failed to extract JB product config") } }() v := reflect.ValueOf(*config.Jetbrains).FieldByNameFunc(func(s string) bool { return strings.ToLower(s) == alias }).Interface() productConfig, ok := v.(*gitpod.JetbrainsProduct) if !ok { return nil } return productConfig } func linkRemotePlugin(launchCtx *LaunchContext) error { remotePluginDir := launchCtx.backendDir + "/plugins/gitpod-remote" _, err := os.Stat(remotePluginDir) if err == nil || !errors.Is(err, os.ErrNotExist) { return nil } // added for backwards compatibility, can be removed in the future sourceDir := "/ide-desktop-plugins/gitpod-remote-" + os.Getenv("JETBRAINS_BACKEND_QUALIFIER") _, err = os.Stat(sourceDir) if err == nil { return os.Symlink(sourceDir, remotePluginDir) } return os.Symlink("/ide-desktop-plugins/gitpod-remote", remotePluginDir) } // 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 func findRiderSolutionFile(root string) (string, error) { slnRegEx := regexp.MustCompile(`^.+\.sln$`) projRegEx := regexp.MustCompile(`^.+\.csproj$`) var slnFiles []string var csprojFiles []string err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } else if slnRegEx.MatchString(info.Name()) { slnFiles = append(slnFiles, path) } else if projRegEx.MatchString(info.Name()) { csprojFiles = append(csprojFiles, path) } return nil }) if err != nil { return "", err } if len(slnFiles) > 0 { return slnFiles[0], nil } else if len(csprojFiles) > 0 { return csprojFiles[0], nil } return root, nil } func resolveProjectContextDir(launchCtx *LaunchContext) string { if launchCtx.alias == "rider" { return launchCtx.riderSolutionFile } return launchCtx.projectDir } func waitForTasksToFinish() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() var conn *grpc.ClientConn var err error for { conn, err = dial(ctx) if err == nil { err = checkTasks(ctx, conn) } if err == nil { return } log.WithError(err).Error("launcher: failed to check tasks status") select { case <-ctx.Done(): return case <-time.After(1 * time.Second): } } } func checkTasks(ctx context.Context, conn *grpc.ClientConn) error { client := supervisor.NewStatusServiceClient(conn) tasksResponse, err := client.TasksStatus(ctx, &supervisor.TasksStatusRequest{Observe: true}) if err != nil { return xerrors.Errorf("failed get tasks status client: %w", err) } for { var runningTasksCounter int resp, err := tasksResponse.Recv() if err != nil { return err } for _, task := range resp.Tasks { if task.State != supervisor.TaskState_closed && task.Presentation.Name != "GITPOD_JB_WARMUP_TASK" { runningTasksCounter++ } } if runningTasksCounter == 0 { break } } return nil } func dial(ctx context.Context) (*grpc.ClientConn, error) { supervisorConn, err := grpc.DialContext(ctx, util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { err = xerrors.Errorf("failed connecting to supervisor: %w", err) } return supervisorConn, err }