Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ide/code/codehelper/main.go
2500 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
"context"
9
"errors"
10
"fmt"
11
"net/url"
12
"os"
13
"path/filepath"
14
"regexp"
15
"strings"
16
"syscall"
17
"time"
18
19
"google.golang.org/grpc"
20
"google.golang.org/grpc/credentials/insecure"
21
yaml "gopkg.in/yaml.v2"
22
23
"github.com/gitpod-io/gitpod/common-go/log"
24
"github.com/gitpod-io/gitpod/common-go/util"
25
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
26
supervisor "github.com/gitpod-io/gitpod/supervisor/api"
27
"github.com/sirupsen/logrus"
28
)
29
30
var (
31
// ServiceName is the name we use for tracing/logging.
32
ServiceName = "codehelper"
33
// Version of this service - set during build.
34
Version = ""
35
)
36
37
const (
38
Code = "/ide/bin/gitpod-code"
39
ProductJsonLocation = "/ide/product.json"
40
WebWorkbenchMainLocation = "/ide/out/vs/workbench/workbench.web.main.js"
41
ServerMainLocation = "/ide/out/vs/server/node/server.main.js"
42
)
43
44
func main() {
45
enableDebug := os.Getenv("SUPERVISOR_DEBUG_ENABLE") == "true"
46
47
log.Init(ServiceName, Version, true, enableDebug)
48
log.Info("codehelper started")
49
startTime := time.Now()
50
51
phaseDone := phaseLogging("ResolveWsInfo")
52
cstate, wsInfo, err := resolveWorkspaceInfo(context.Background())
53
if err != nil || wsInfo == nil {
54
log.WithError(err).WithField("wsInfo", wsInfo).Error("resolve workspace info failed")
55
return
56
}
57
phaseDone()
58
59
url, err := url.Parse(wsInfo.GitpodHost)
60
if err != nil {
61
log.WithError(err).Errorf("failed to parse GitpodHost %s", wsInfo.GitpodHost)
62
return
63
}
64
domain := url.Hostname()
65
66
// code server args install extension with id
67
args := []string{}
68
69
if enableDebug {
70
args = append(args, "--inspect", "--log=trace")
71
} else {
72
switch log.Log.Logger.GetLevel() {
73
case logrus.PanicLevel:
74
args = append(args, "--log=critical")
75
case logrus.FatalLevel:
76
args = append(args, "--log=critical")
77
case logrus.ErrorLevel:
78
args = append(args, "--log=error")
79
case logrus.WarnLevel:
80
args = append(args, "--log=warn")
81
case logrus.InfoLevel:
82
args = append(args, "--log=info")
83
case logrus.DebugLevel:
84
args = append(args, "--log=debug")
85
case logrus.TraceLevel:
86
args = append(args, "--log=trace")
87
}
88
}
89
90
args = append(args, "--install-builtin-extension", "gitpod.gitpod-theme")
91
92
wsContextUrl := wsInfo.GetWorkspaceContextUrl()
93
if ctxUrl, err := url.Parse(wsContextUrl); err == nil {
94
if ctxUrl.Host == "github.com" {
95
log.Info("ws context url is from github.com, install builtin extension github.vscode-pull-request-github")
96
args = append(args, "--install-builtin-extension", "github.vscode-pull-request-github")
97
}
98
} else {
99
log.WithError(err).WithField("wsContextUrl", wsContextUrl).Error("parse ws context url failed")
100
}
101
102
phaseDone = phaseLogging("GetExtensions")
103
uniqMap := map[string]struct{}{}
104
extensions, err := getExtensions(wsInfo.GetCheckoutLocation())
105
if err != nil {
106
log.WithError(err).Error("get extensions failed")
107
}
108
log.WithField("ext", extensions).Info("get extensions")
109
for _, ext := range extensions {
110
if _, ok := uniqMap[ext.Location]; ok {
111
continue
112
}
113
uniqMap[ext.Location] = struct{}{}
114
// don't install url extension for backup, see https://github.com/microsoft/vscode/issues/143617#issuecomment-1047881213
115
if cstate.Source != supervisor.ContentSource_from_backup || !ext.IsUrl {
116
args = append(args, "--install-extension", ext.Location)
117
}
118
}
119
phaseDone()
120
121
// install extensions and run code server with exec
122
args = append(args, os.Args[1:]...)
123
args = append(args, "--do-not-sync")
124
args = append(args, "--start-server")
125
cmdEnv := append(os.Environ(), fmt.Sprintf("GITPOD_CODE_HOST=%s", domain))
126
log.WithField("cost", time.Now().Local().Sub(startTime).Milliseconds()).Info("starting server")
127
if err := syscall.Exec(Code, append([]string{"gitpod-code"}, args...), cmdEnv); err != nil {
128
log.WithError(err).Error("install ext and start code server failed")
129
}
130
}
131
132
func resolveWorkspaceInfo(ctx context.Context) (*supervisor.ContentStatusResponse, *supervisor.WorkspaceInfoResponse, error) {
133
resolve := func(ctx context.Context) (cstate *supervisor.ContentStatusResponse, wsInfo *supervisor.WorkspaceInfoResponse, err error) {
134
supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))
135
if err != nil {
136
err = errors.New("dial supervisor failed: " + err.Error())
137
return
138
}
139
defer supervisorConn.Close()
140
if cstate, err = supervisor.NewStatusServiceClient(supervisorConn).ContentStatus(ctx, &supervisor.ContentStatusRequest{}); err != nil {
141
err = errors.New("get content state failed: " + err.Error())
142
return
143
}
144
if wsInfo, err = supervisor.NewInfoServiceClient(supervisorConn).WorkspaceInfo(ctx, &supervisor.WorkspaceInfoRequest{}); err != nil {
145
err = errors.New("get workspace info failed: " + err.Error())
146
return
147
}
148
return
149
}
150
// try resolve workspace info 10 times
151
for attempt := 0; attempt < 10; attempt++ {
152
if cstate, wsInfo, err := resolve(ctx); err != nil {
153
log.WithError(err).Error("resolve workspace info failed")
154
time.Sleep(1 * time.Second)
155
} else {
156
return cstate, wsInfo, err
157
}
158
}
159
return nil, nil, errors.New("failed with attempt 10 times")
160
}
161
162
type Extension struct {
163
IsUrl bool `json:"is_url"`
164
Location string `json:"location"`
165
}
166
167
func getExtensions(repoRoot string) (extensions []Extension, err error) {
168
if repoRoot == "" {
169
err = errors.New("repoRoot is empty")
170
return
171
}
172
data, err := os.ReadFile(filepath.Join(repoRoot, ".gitpod.yml"))
173
if err != nil {
174
// .gitpod.yml not exist is ok
175
if errors.Is(err, os.ErrNotExist) {
176
err = nil
177
return
178
}
179
err = errors.New("read .gitpod.yml file failed: " + err.Error())
180
return
181
}
182
var config *gitpod.GitpodConfig
183
if err = yaml.Unmarshal(data, &config); err != nil {
184
err = errors.New("unmarshal .gitpod.yml file failed" + err.Error())
185
return
186
}
187
if config == nil || config.Vscode == nil {
188
return
189
}
190
for _, ext := range config.Vscode.Extensions {
191
lowerCaseExtension := strings.ToLower(ext)
192
if isUrl(lowerCaseExtension) {
193
extensions = append(extensions, Extension{
194
IsUrl: true,
195
Location: ext,
196
})
197
} else {
198
extensions = append(extensions, Extension{
199
IsUrl: false,
200
Location: lowerCaseExtension,
201
})
202
}
203
}
204
return
205
}
206
207
func isUrl(lowerCaseIdOrUrl string) bool {
208
isUrl, _ := regexp.MatchString(`http[s]?://`, lowerCaseIdOrUrl)
209
return isUrl
210
}
211
212
func phaseLogging(phase string) context.CancelFunc {
213
ctx, cancel := context.WithCancel(context.Background())
214
start := time.Now()
215
log.WithField("phase", phase).Info("phase start")
216
go func() {
217
<-ctx.Done()
218
duration := time.Now().Local().Sub(start).Seconds()
219
log.WithField("phase", phase).WithField("duration", duration).Info("phase end")
220
}()
221
return cancel
222
}
223
224