Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/pkg/terminal/service.go
2500 views
1
// Copyright (c) 2020 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 terminal
6
7
import (
8
"context"
9
"fmt"
10
"io"
11
"os"
12
"os/exec"
13
"path/filepath"
14
"syscall"
15
"time"
16
17
"github.com/creack/pty"
18
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
19
"google.golang.org/grpc"
20
"google.golang.org/grpc/codes"
21
"google.golang.org/grpc/credentials/insecure"
22
"google.golang.org/grpc/status"
23
24
"github.com/gitpod-io/gitpod/common-go/log"
25
"github.com/gitpod-io/gitpod/supervisor/api"
26
)
27
28
// NewMuxTerminalService creates a new terminal service.
29
func NewMuxTerminalService(m *Mux) *MuxTerminalService {
30
shell := os.Getenv("SHELL")
31
if shell == "" {
32
shell = "/bin/bash"
33
}
34
return &MuxTerminalService{
35
Mux: m,
36
DefaultWorkdir: "/workspace",
37
DefaultShell: shell,
38
Env: os.Environ(),
39
}
40
}
41
42
// MuxTerminalService implements the terminal service API using a terminal Mux.
43
type MuxTerminalService struct {
44
Mux *Mux
45
46
DefaultWorkdir string
47
// DefaultWorkdirProvider allows dynamically to compute workdir
48
// if returns empty string then DefaultWorkdir is used
49
DefaultWorkdirProvider func() string
50
51
DefaultShell string
52
Env []string
53
DefaultCreds *syscall.Credential
54
DefaultAmbientCaps []uintptr
55
56
api.UnimplementedTerminalServiceServer
57
}
58
59
// RegisterGRPC registers a gRPC service.
60
func (srv *MuxTerminalService) RegisterGRPC(s *grpc.Server) {
61
api.RegisterTerminalServiceServer(s, srv)
62
}
63
64
// RegisterREST registers a REST service.
65
func (srv *MuxTerminalService) RegisterREST(mux *runtime.ServeMux, grpcEndpoint string) error {
66
return api.RegisterTerminalServiceHandlerFromEndpoint(context.Background(), mux, grpcEndpoint, []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())})
67
}
68
69
// Open opens a new terminal running the shell.
70
func (srv *MuxTerminalService) Open(ctx context.Context, req *api.OpenTerminalRequest) (*api.OpenTerminalResponse, error) {
71
return srv.OpenWithOptions(ctx, req, TermOptions{
72
ReadTimeout: 5 * time.Second,
73
Annotations: req.Annotations,
74
})
75
}
76
77
// OpenWithOptions opens a new terminal running the shell with given options.
78
// req.Annotations override options.Annotations.
79
func (srv *MuxTerminalService) OpenWithOptions(ctx context.Context, req *api.OpenTerminalRequest, options TermOptions) (*api.OpenTerminalResponse, error) {
80
shell := req.Shell
81
if shell == "" {
82
shell = srv.DefaultShell
83
}
84
cmd := exec.Command(shell, req.ShellArgs...)
85
if srv.DefaultCreds != nil {
86
cmd.SysProcAttr = &syscall.SysProcAttr{
87
Credential: srv.DefaultCreds,
88
}
89
}
90
if req.Workdir != "" {
91
cmd.Dir = req.Workdir
92
} else if srv.DefaultWorkdirProvider != nil {
93
cmd.Dir = srv.DefaultWorkdirProvider()
94
}
95
if cmd.Dir == "" {
96
cmd.Dir = srv.DefaultWorkdir
97
}
98
cmd.Env = append(srv.Env, "TERM=xterm-256color")
99
for key, value := range req.Env {
100
cmd.Env = append(cmd.Env, fmt.Sprintf("%v=%v", key, value))
101
}
102
for k, v := range req.Annotations {
103
options.Annotations[k] = v
104
}
105
if req.Size != nil {
106
options.Size = &pty.Winsize{
107
Cols: uint16(req.Size.Cols),
108
Rows: uint16(req.Size.Rows),
109
X: uint16(req.Size.WidthPx),
110
Y: uint16(req.Size.HeightPx),
111
}
112
}
113
114
if srv.DefaultAmbientCaps != nil {
115
if cmd.SysProcAttr == nil {
116
cmd.SysProcAttr = &syscall.SysProcAttr{}
117
}
118
cmd.SysProcAttr.AmbientCaps = srv.DefaultAmbientCaps
119
}
120
121
alias, err := srv.Mux.Start(cmd, options)
122
if err != nil {
123
return nil, status.Error(codes.Internal, err.Error())
124
}
125
126
// starterToken is just relevant for the service, hence it's not exposed at the Start() call
127
var starterToken string
128
term := srv.Mux.terms[alias]
129
if term != nil {
130
starterToken = term.StarterToken
131
}
132
133
terminal, found := srv.get(alias)
134
if !found {
135
return nil, status.Error(codes.NotFound, "terminal not found")
136
}
137
return &api.OpenTerminalResponse{
138
Terminal: terminal,
139
StarterToken: starterToken,
140
}, nil
141
}
142
143
// Close closes a terminal for the given alias.
144
func (srv *MuxTerminalService) Shutdown(ctx context.Context, req *api.ShutdownTerminalRequest) (*api.ShutdownTerminalResponse, error) {
145
err := srv.Mux.CloseTerminal(ctx, req.Alias, req.ForceSuccess)
146
if err == ErrNotFound {
147
return nil, status.Error(codes.NotFound, err.Error())
148
}
149
if err != nil {
150
return nil, status.Error(codes.Internal, err.Error())
151
}
152
return &api.ShutdownTerminalResponse{}, nil
153
}
154
155
// List lists all open terminals.
156
func (srv *MuxTerminalService) List(ctx context.Context, req *api.ListTerminalsRequest) (*api.ListTerminalsResponse, error) {
157
srv.Mux.mu.RLock()
158
defer srv.Mux.mu.RUnlock()
159
160
res := make([]*api.Terminal, 0, len(srv.Mux.terms))
161
for _, alias := range srv.Mux.aliases {
162
term, ok := srv.get(alias)
163
if !ok {
164
continue
165
}
166
res = append(res, term)
167
}
168
169
return &api.ListTerminalsResponse{
170
Terminals: res,
171
}, nil
172
}
173
174
// Get returns an open terminal info.
175
func (srv *MuxTerminalService) Get(ctx context.Context, req *api.GetTerminalRequest) (*api.Terminal, error) {
176
srv.Mux.mu.RLock()
177
defer srv.Mux.mu.RUnlock()
178
term, ok := srv.get(req.Alias)
179
if !ok {
180
return nil, status.Error(codes.NotFound, "terminal not found")
181
}
182
return term, nil
183
}
184
185
func (srv *MuxTerminalService) get(alias string) (*api.Terminal, bool) {
186
term, ok := srv.Mux.terms[alias]
187
if !ok {
188
return nil, false
189
}
190
191
var (
192
pid int64
193
cwd string
194
err error
195
)
196
if proc := term.Command.Process; proc != nil {
197
pid = int64(proc.Pid)
198
cwd, err = filepath.EvalSymlinks(fmt.Sprintf("/proc/%d/cwd", pid))
199
if err != nil {
200
log.WithError(err).WithField("pid", pid).Warn("unable to resolve terminal's current working dir")
201
cwd = term.Command.Dir
202
}
203
}
204
205
title, titleSource, err := term.GetTitle()
206
if err != nil {
207
log.WithError(err).WithField("pid", pid).Warn("unable to resolve terminal's title")
208
}
209
210
return &api.Terminal{
211
Alias: alias,
212
Command: term.Command.Args,
213
Pid: pid,
214
InitialWorkdir: term.Command.Dir,
215
CurrentWorkdir: cwd,
216
Annotations: term.GetAnnotations(),
217
Title: title,
218
TitleSource: titleSource,
219
}, true
220
}
221
222
// Listen listens to a terminal.
223
func (srv *MuxTerminalService) Listen(req *api.ListenTerminalRequest, resp api.TerminalService_ListenServer) error {
224
srv.Mux.mu.RLock()
225
term, ok := srv.Mux.terms[req.Alias]
226
srv.Mux.mu.RUnlock()
227
if !ok {
228
return status.Error(codes.NotFound, "terminal not found")
229
}
230
stdout := term.Stdout.Listen()
231
defer stdout.Close()
232
233
log.WithField("alias", req.Alias).Info("new terminal client")
234
defer log.WithField("alias", req.Alias).Info("terminal client left")
235
236
errchan := make(chan error, 1)
237
messages := make(chan *api.ListenTerminalResponse, 1)
238
go func() {
239
for {
240
buf := make([]byte, 4096)
241
n, err := stdout.Read(buf)
242
if err == io.EOF {
243
break
244
}
245
if err != nil {
246
errchan <- err
247
return
248
}
249
messages <- &api.ListenTerminalResponse{Output: &api.ListenTerminalResponse_Data{Data: buf[:n]}}
250
}
251
252
state, err := term.Wait()
253
if err != nil {
254
errchan <- err
255
return
256
}
257
258
messages <- &api.ListenTerminalResponse{Output: &api.ListenTerminalResponse_ExitCode{ExitCode: int32(state.ExitCode())}}
259
errchan <- io.EOF
260
}()
261
go func() {
262
title, titleSource, _ := term.GetTitle()
263
messages <- &api.ListenTerminalResponse{Output: &api.ListenTerminalResponse_Title{Title: title}, TitleSource: titleSource}
264
265
t := time.NewTicker(200 * time.Millisecond)
266
defer t.Stop()
267
for {
268
select {
269
case <-resp.Context().Done():
270
return
271
case <-t.C:
272
newTitle, newTitleSource, _ := term.GetTitle()
273
if title == newTitle && titleSource == newTitleSource {
274
continue
275
}
276
title = newTitle
277
titleSource = newTitleSource
278
messages <- &api.ListenTerminalResponse{Output: &api.ListenTerminalResponse_Title{Title: title}, TitleSource: titleSource}
279
}
280
}
281
}()
282
for {
283
var err error
284
select {
285
case message := <-messages:
286
err = resp.Send(message)
287
case err = <-errchan:
288
case <-resp.Context().Done():
289
return nil
290
}
291
if err == io.EOF {
292
// EOF isn't really an error here
293
return nil
294
}
295
if err != nil {
296
return status.Error(codes.Internal, err.Error())
297
}
298
}
299
}
300
301
// Write writes to a terminal.
302
func (srv *MuxTerminalService) Write(ctx context.Context, req *api.WriteTerminalRequest) (*api.WriteTerminalResponse, error) {
303
srv.Mux.mu.RLock()
304
term, ok := srv.Mux.terms[req.Alias]
305
srv.Mux.mu.RUnlock()
306
if !ok {
307
return nil, status.Error(codes.NotFound, "terminal not found")
308
}
309
310
n, err := term.PTY.Write(req.Stdin)
311
if err != nil {
312
return nil, status.Error(codes.Internal, err.Error())
313
}
314
return &api.WriteTerminalResponse{BytesWritten: uint32(n)}, nil
315
}
316
317
// SetSize sets the terminal's size.
318
func (srv *MuxTerminalService) SetSize(ctx context.Context, req *api.SetTerminalSizeRequest) (*api.SetTerminalSizeResponse, error) {
319
srv.Mux.mu.RLock()
320
term, ok := srv.Mux.terms[req.Alias]
321
srv.Mux.mu.RUnlock()
322
if !ok {
323
return nil, status.Error(codes.NotFound, "terminal not found")
324
}
325
326
// Setting the size only works with the starter token or when forcing it.
327
// This protects us from multiple listener mangling the terminal.
328
if !(req.GetForce() || req.GetToken() == term.StarterToken) {
329
return nil, status.Error(codes.FailedPrecondition, "wrong token or force not set")
330
}
331
332
err := pty.Setsize(term.PTY, &pty.Winsize{
333
Cols: uint16(req.Size.Cols),
334
Rows: uint16(req.Size.Rows),
335
X: uint16(req.Size.WidthPx),
336
Y: uint16(req.Size.HeightPx),
337
})
338
if err != nil {
339
return nil, status.Error(codes.Internal, err.Error())
340
}
341
342
return &api.SetTerminalSizeResponse{}, nil
343
}
344
345
// SetTitle sets the terminal's title.
346
func (srv *MuxTerminalService) SetTitle(ctx context.Context, req *api.SetTerminalTitleRequest) (*api.SetTerminalTitleResponse, error) {
347
srv.Mux.mu.RLock()
348
term, ok := srv.Mux.terms[req.Alias]
349
srv.Mux.mu.RUnlock()
350
if !ok {
351
return nil, status.Error(codes.NotFound, "terminal not found")
352
}
353
term.SetTitle(req.Title)
354
return &api.SetTerminalTitleResponse{}, nil
355
}
356
357
// UpdateAnnotations sets the terminal's title.
358
func (srv *MuxTerminalService) UpdateAnnotations(ctx context.Context, req *api.UpdateTerminalAnnotationsRequest) (*api.UpdateTerminalAnnotationsResponse, error) {
359
srv.Mux.mu.RLock()
360
term, ok := srv.Mux.terms[req.Alias]
361
srv.Mux.mu.RUnlock()
362
if !ok {
363
return nil, status.Error(codes.NotFound, "terminal not found")
364
}
365
term.UpdateAnnotations(req.Changed, req.Deleted)
366
return &api.UpdateTerminalAnnotationsResponse{}, nil
367
}
368
369