Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/pkg/terminal/terminal.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
"bytes"
9
"context"
10
"errors"
11
"fmt"
12
"io"
13
"os"
14
"os/exec"
15
"strings"
16
"sync"
17
"syscall"
18
"time"
19
20
_pty "github.com/creack/pty"
21
"github.com/google/uuid"
22
23
"github.com/sirupsen/logrus"
24
"golang.org/x/sys/unix"
25
"golang.org/x/xerrors"
26
27
"github.com/gitpod-io/gitpod/common-go/log"
28
"github.com/gitpod-io/gitpod/common-go/process"
29
"github.com/gitpod-io/gitpod/supervisor/api"
30
)
31
32
const (
33
CBAUD = 0010017 // CBAUD Serial speed settings
34
CBAUDEX = 0010000 // CBAUDX Serial speed settings
35
36
DEFAULT_COLS = 80
37
DEFAULT_ROWS = 24
38
)
39
40
// NewMux creates a new terminal mux.
41
func NewMux() *Mux {
42
return &Mux{
43
terms: make(map[string]*Term),
44
}
45
}
46
47
// Mux can mux pseudo-terminals.
48
type Mux struct {
49
aliases []string
50
terms map[string]*Term
51
mu sync.RWMutex
52
}
53
54
// Get returns a terminal for the given alias.
55
func (m *Mux) Get(alias string) (*Term, bool) {
56
m.mu.RLock()
57
defer m.mu.RUnlock()
58
term, ok := m.terms[alias]
59
return term, ok
60
}
61
62
// Start starts a new command in its own pseudo-terminal and returns an alias
63
// for that pseudo terminal.
64
func (m *Mux) Start(cmd *exec.Cmd, options TermOptions) (alias string, err error) {
65
m.mu.Lock()
66
defer m.mu.Unlock()
67
68
uid, err := uuid.NewRandom()
69
if err != nil {
70
return "", xerrors.Errorf("cannot produce alias: %w", err)
71
}
72
alias = uid.String()
73
74
term, err := newTerm(alias, cmd, options)
75
if err != nil {
76
return "", err
77
}
78
m.aliases = append(m.aliases, alias)
79
m.terms[alias] = term
80
81
log.WithField("alias", alias).WithField("cmd", cmd.Path).Info("started new terminal")
82
83
go func() {
84
term.waitErr = cmd.Wait()
85
close(term.waitDone)
86
_ = m.CloseTerminal(context.Background(), alias, false)
87
}()
88
89
return alias, nil
90
}
91
92
// Close closes all terminals.
93
// force kills it's processes when the context gets cancelled
94
func (m *Mux) Close(ctx context.Context) {
95
m.mu.Lock()
96
defer m.mu.Unlock()
97
98
wg := sync.WaitGroup{}
99
for alias, term := range m.terms {
100
wg.Add(1)
101
k := alias
102
v := term
103
go func() {
104
defer wg.Done()
105
err := v.Close(ctx)
106
if err != nil {
107
log.WithError(err).WithField("alias", k).Warn("Error while closing pseudo-terminal")
108
}
109
}()
110
}
111
wg.Wait()
112
113
m.aliases = m.aliases[:0]
114
for k := range m.terms {
115
delete(m.terms, k)
116
}
117
}
118
119
// CloseTerminal closes a terminal and ends the process that runs in it.
120
func (m *Mux) CloseTerminal(ctx context.Context, alias string, forceSuccess bool) error {
121
m.mu.Lock()
122
defer m.mu.Unlock()
123
124
return m.doClose(ctx, alias, forceSuccess)
125
}
126
127
// doClose closes a terminal and ends the process that runs in it.
128
// First, the process receives SIGTERM and is given gracePeriod time
129
// to stop. If it still runs after that time, it receives SIGKILL.
130
//
131
// Callers are expected to hold mu.
132
func (m *Mux) doClose(ctx context.Context, alias string, forceSuccess bool) error {
133
term, ok := m.terms[alias]
134
if !ok {
135
return ErrNotFound
136
}
137
138
log := log.WithField("alias", alias)
139
log.Info("closing terminal")
140
141
if forceSuccess {
142
term.ForceSuccess = true
143
}
144
145
err := term.Close(ctx)
146
if err != nil {
147
log.WithError(err).Warn("Error while closing pseudo-terminal")
148
}
149
150
i := 0
151
for i < len(m.aliases) && m.aliases[i] != alias {
152
i++
153
}
154
if i != len(m.aliases) {
155
m.aliases = append(m.aliases[:i], m.aliases[i+1:]...)
156
}
157
delete(m.terms, alias)
158
159
return nil
160
}
161
162
// terminalBacklogSize is the number of bytes of output we'll store in RAM for each terminal.
163
// The higher this number is, the better the UX, but the higher the resource requirements are.
164
// For now we assume an average of five terminals per workspace, which makes this consume 1MiB of RAM.
165
const terminalBacklogSize = 256 << 10
166
167
func newTerm(alias string, cmd *exec.Cmd, options TermOptions) (*Term, error) {
168
token, err := uuid.NewRandom()
169
if err != nil {
170
return nil, err
171
}
172
173
recorder, err := NewRingBuffer(terminalBacklogSize)
174
if err != nil {
175
return nil, err
176
}
177
178
timeout := options.ReadTimeout
179
if timeout == 0 {
180
timeout = NoTimeout
181
}
182
183
annotations := options.Annotations
184
if annotations == nil {
185
annotations = make(map[string]string)
186
}
187
188
size := _pty.Winsize{Cols: DEFAULT_COLS, Rows: DEFAULT_ROWS}
189
if options.Size != nil {
190
if options.Size.Cols != 0 {
191
size.Cols = options.Size.Cols
192
}
193
if options.Size.Rows != 0 {
194
size.Rows = options.Size.Rows
195
}
196
}
197
198
pty, pts, err := _pty.Open()
199
if err != nil {
200
pts.Close()
201
pty.Close()
202
return nil, xerrors.Errorf("cannot start PTY: %w", err)
203
}
204
205
if err := _pty.Setsize(pty, &size); err != nil {
206
pts.Close()
207
pty.Close()
208
return nil, err
209
}
210
211
// Set up terminal (from node-pty)
212
var attr unix.Termios
213
attr.Iflag = unix.ICRNL | unix.IXON | unix.IXANY | unix.IMAXBEL | unix.BRKINT | syscall.IUTF8
214
attr.Oflag = unix.OPOST | unix.ONLCR
215
attr.Cflag = unix.CREAD | unix.CS8 | unix.HUPCL
216
attr.Lflag = unix.ICANON | unix.ISIG | unix.IEXTEN | unix.ECHO | unix.ECHOE | unix.ECHOK | unix.ECHOKE | unix.ECHOCTL
217
attr.Cc[unix.VEOF] = 4
218
attr.Cc[unix.VEOL] = 0xff
219
attr.Cc[unix.VEOL2] = 0xff
220
attr.Cc[unix.VERASE] = 0x7f
221
attr.Cc[unix.VWERASE] = 23
222
attr.Cc[unix.VKILL] = 21
223
attr.Cc[unix.VREPRINT] = 18
224
attr.Cc[unix.VINTR] = 3
225
attr.Cc[unix.VQUIT] = 0x1c
226
attr.Cc[unix.VSUSP] = 26
227
attr.Cc[unix.VSTART] = 17
228
attr.Cc[unix.VSTOP] = 19
229
attr.Cc[unix.VLNEXT] = 22
230
attr.Cc[unix.VDISCARD] = 15
231
attr.Cc[unix.VMIN] = 1
232
attr.Cc[unix.VTIME] = 0
233
234
attr.Ispeed = unix.B38400
235
attr.Ospeed = unix.B38400
236
attr.Cflag &^= CBAUD | CBAUDEX
237
attr.Cflag |= unix.B38400
238
239
err = unix.IoctlSetTermios(int(pts.Fd()), syscall.TCSETS, &attr)
240
if err != nil {
241
pts.Close()
242
pty.Close()
243
return nil, err
244
}
245
246
cmd.Stdout = pts
247
cmd.Stderr = pts
248
cmd.Stdin = pts
249
250
if cmd.SysProcAttr == nil {
251
cmd.SysProcAttr = &syscall.SysProcAttr{}
252
}
253
cmd.SysProcAttr.Setsid = true
254
cmd.SysProcAttr.Setctty = true
255
256
if err := cmd.Start(); err != nil {
257
pts.Close()
258
pty.Close()
259
return nil, err
260
}
261
262
res := &Term{
263
PTY: pty,
264
pts: pts,
265
Command: cmd,
266
Stdout: &multiWriter{
267
timeout: timeout,
268
listener: make(map[*multiWriterListener]struct{}),
269
recorder: recorder,
270
logStdout: options.LogToStdout,
271
logLabel: alias,
272
},
273
annotations: annotations,
274
defaultTitle: options.Title,
275
276
StarterToken: token.String(),
277
278
waitDone: make(chan struct{}),
279
}
280
281
//nolint:errcheck
282
go io.Copy(res.Stdout, pty)
283
return res, nil
284
}
285
286
// NoTimeout means that listener can block read forever
287
var NoTimeout time.Duration = 1<<63 - 1
288
289
// TermOptions is a pseudo-terminal configuration.
290
type TermOptions struct {
291
// timeout after which a listener is dropped. Use 0 for no timeout.
292
ReadTimeout time.Duration
293
294
// Annotations are user-defined metadata that's attached to a terminal
295
Annotations map[string]string
296
297
// Size describes the terminal size.
298
Size *_pty.Winsize
299
300
// Title describes the terminal title.
301
Title string
302
303
// LogToStdout forwards the terminal's stdout to supervisor's stdout
304
LogToStdout bool
305
}
306
307
// Term is a pseudo-terminal.
308
type Term struct {
309
PTY *os.File
310
pts *os.File
311
312
Command *exec.Cmd
313
StarterToken string
314
315
mu sync.RWMutex
316
closed bool
317
318
annotations map[string]string
319
defaultTitle string
320
title string
321
322
// ForceSuccess overrides the process' exit code to 0
323
ForceSuccess bool
324
325
Stdout *multiWriter
326
327
waitErr error
328
waitDone chan struct{}
329
}
330
331
func (term *Term) GetTitle() (string, api.TerminalTitleSource, error) {
332
term.mu.RLock()
333
title := term.title
334
term.mu.RUnlock()
335
if title != "" {
336
return title, api.TerminalTitleSource_api, nil
337
}
338
var b bytes.Buffer
339
defaultTitle := term.defaultTitle
340
b.WriteString(defaultTitle)
341
command, err := term.resolveForegroundCommand()
342
if defaultTitle != "" && command != "" {
343
b.WriteString(": ")
344
}
345
b.WriteString(command)
346
return b.String(), api.TerminalTitleSource_process, err
347
}
348
349
func (term *Term) SetTitle(title string) {
350
term.mu.Lock()
351
defer term.mu.Unlock()
352
term.title = title
353
}
354
355
func (term *Term) GetAnnotations() map[string]string {
356
term.mu.RLock()
357
defer term.mu.RUnlock()
358
annotations := make(map[string]string, len(term.annotations))
359
for k, v := range term.annotations {
360
annotations[k] = v
361
}
362
return annotations
363
}
364
365
func (term *Term) UpdateAnnotations(changed map[string]string, deleted []string) {
366
term.mu.Lock()
367
defer term.mu.Unlock()
368
for k, v := range changed {
369
term.annotations[k] = v
370
}
371
for _, k := range deleted {
372
delete(term.annotations, k)
373
}
374
}
375
376
func (term *Term) resolveForegroundCommand() (string, error) {
377
pgrp, err := unix.IoctlGetInt(int(term.PTY.Fd()), unix.TIOCGPGRP)
378
if err != nil {
379
return "", err
380
}
381
content, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pgrp))
382
if err != nil {
383
return "", err
384
}
385
end := bytes.Index(content, []byte{0})
386
if end != -1 {
387
content = content[:end]
388
}
389
start := bytes.LastIndex(content, []byte{os.PathSeparator})
390
if start != -1 {
391
content = content[(start + 1):]
392
}
393
return string(content), nil
394
}
395
396
// Wait waits for the terminal to exit and returns the resulted process state.
397
func (term *Term) Wait() (*os.ProcessState, error) {
398
<-term.waitDone
399
return term.Command.ProcessState, term.waitErr
400
}
401
402
func (term *Term) Close(ctx context.Context) error {
403
term.mu.Lock()
404
defer term.mu.Unlock()
405
406
if term.closed {
407
return nil
408
}
409
410
term.closed = true
411
412
var commandErr error
413
if term.Command.Process != nil {
414
commandErr = process.TerminateSync(ctx, term.Command.Process.Pid)
415
if process.IsNotChildProcess(commandErr) {
416
commandErr = nil
417
}
418
}
419
420
writeErr := term.Stdout.Close()
421
422
slaveErr := errors.New("Slave FD nil")
423
if term.pts != nil {
424
slaveErr = term.pts.Close()
425
}
426
masterErr := errors.New("Master FD nil")
427
if term.PTY != nil {
428
masterErr = term.PTY.Close()
429
}
430
431
var errs []string
432
if commandErr != nil {
433
errs = append(errs, "Process: cannot terminate process: "+commandErr.Error())
434
}
435
if writeErr != nil {
436
errs = append(errs, "Multiwriter: "+writeErr.Error())
437
}
438
if slaveErr != nil {
439
errs = append(errs, "Slave: "+slaveErr.Error())
440
}
441
if masterErr != nil {
442
errs = append(errs, "Master: "+masterErr.Error())
443
}
444
445
if len(errs) > 0 {
446
return errors.New(strings.Join(errs, " "))
447
}
448
449
return nil
450
}
451
452
// multiWriter is like io.MultiWriter, except that we can listener at runtime.
453
type multiWriter struct {
454
timeout time.Duration
455
closed bool
456
mu sync.RWMutex
457
listener map[*multiWriterListener]struct{}
458
// ring buffer to record last 256kb of pty output
459
// new listener is initialized with the latest recodring first
460
recorder *RingBuffer
461
462
logStdout bool
463
logLabel string
464
}
465
466
var (
467
// ErrNotFound means the terminal was not found.
468
ErrNotFound = errors.New("not found")
469
// ErrReadTimeout happens when a listener takes too long to read.
470
ErrReadTimeout = errors.New("read timeout")
471
)
472
473
type multiWriterListener struct {
474
io.Reader
475
timeout time.Duration
476
477
closed bool
478
once sync.Once
479
closeErr error
480
closeChan chan struct{}
481
cchan chan []byte
482
done chan struct{}
483
}
484
485
func (l *multiWriterListener) Close() error {
486
return l.CloseWithError(nil)
487
}
488
489
func (l *multiWriterListener) CloseWithError(err error) error {
490
l.once.Do(func() {
491
if err != nil {
492
l.closeErr = err
493
}
494
l.closed = true
495
close(l.closeChan)
496
497
// actual cleanup happens in a go routine started by Listen()
498
})
499
return nil
500
}
501
502
func (l *multiWriterListener) Done() <-chan struct{} {
503
return l.closeChan
504
}
505
506
type closedTerminalListener struct{}
507
508
func (closedTerminalListener) Read(p []byte) (n int, err error) {
509
return 0, io.EOF
510
}
511
512
var closedListener = io.NopCloser(closedTerminalListener{})
513
514
// TermListenOptions is a configuration to listen to the pseudo-terminal .
515
type TermListenOptions struct {
516
// timeout after which a listener is dropped. Use 0 for default timeout.
517
ReadTimeout time.Duration
518
}
519
520
// Listen listens in on the multi-writer stream.
521
func (mw *multiWriter) Listen() io.ReadCloser {
522
return mw.ListenWithOptions(TermListenOptions{
523
ReadTimeout: 0,
524
})
525
}
526
527
// Listen listens in on the multi-writer stream with given options.
528
func (mw *multiWriter) ListenWithOptions(options TermListenOptions) io.ReadCloser {
529
mw.mu.Lock()
530
defer mw.mu.Unlock()
531
532
if mw.closed {
533
return closedListener
534
}
535
536
timeout := options.ReadTimeout
537
if timeout == 0 {
538
timeout = mw.timeout
539
}
540
r, w := io.Pipe()
541
cchan, done, closeChan := make(chan []byte), make(chan struct{}, 1), make(chan struct{}, 1)
542
res := &multiWriterListener{
543
Reader: r,
544
cchan: cchan,
545
done: done,
546
closeChan: closeChan,
547
timeout: timeout,
548
}
549
550
recording := mw.recorder.Bytes()
551
go func() {
552
_, _ = w.Write(recording)
553
554
// copy bytes from channel to writer.
555
// Note: we close the writer independently of the write operation s.t. we don't
556
// block the closing because the write's blocking.
557
for b := range cchan {
558
n, err := w.Write(b)
559
done <- struct{}{}
560
if err == nil && n != len(b) {
561
err = io.ErrShortWrite
562
}
563
if err != nil {
564
_ = res.CloseWithError(err)
565
}
566
}
567
}()
568
go func() {
569
// listener cleanup on close
570
<-closeChan
571
572
if res.closeErr != nil {
573
log.WithError(res.closeErr).Error("terminal listener droped out")
574
w.CloseWithError(res.closeErr)
575
} else {
576
w.Close()
577
}
578
579
mw.mu.Lock()
580
defer mw.mu.Unlock()
581
close(cchan)
582
583
delete(mw.listener, res)
584
}()
585
586
mw.listener[res] = struct{}{}
587
588
return res
589
}
590
591
func (mw *multiWriter) Write(p []byte) (n int, err error) {
592
mw.mu.Lock()
593
defer mw.mu.Unlock()
594
595
mw.recorder.Write(p)
596
if mw.logStdout {
597
log.WithFields(logrus.Fields{
598
"terminalOutput": true,
599
"label": mw.logLabel,
600
}).Info(string(p))
601
}
602
603
for lstr := range mw.listener {
604
if lstr.closed {
605
continue
606
}
607
608
select {
609
case lstr.cchan <- p:
610
case <-time.After(lstr.timeout):
611
lstr.CloseWithError(ErrReadTimeout)
612
}
613
614
select {
615
case <-lstr.done:
616
case <-time.After(lstr.timeout):
617
lstr.CloseWithError(ErrReadTimeout)
618
}
619
}
620
return len(p), nil
621
}
622
623
func (mw *multiWriter) Close() error {
624
mw.mu.Lock()
625
defer mw.mu.Unlock()
626
627
mw.closed = true
628
629
var err error
630
for w := range mw.listener {
631
cerr := w.Close()
632
if cerr != nil {
633
err = cerr
634
}
635
}
636
return err
637
}
638
639
func (mw *multiWriter) ListenerCount() int {
640
mw.mu.Lock()
641
defer mw.mu.Unlock()
642
643
return len(mw.listener)
644
}
645
646