Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/cmd/terminal-attach.go
2498 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 cmd
6
7
import (
8
"context"
9
"fmt"
10
"io"
11
"os"
12
"os/signal"
13
"syscall"
14
"time"
15
16
"github.com/creack/pty"
17
"github.com/spf13/cobra"
18
"golang.org/x/term"
19
20
"github.com/gitpod-io/gitpod/common-go/log"
21
"github.com/gitpod-io/gitpod/supervisor/api"
22
)
23
24
var terminalAttachOpts struct {
25
Interactive bool
26
ForceResize bool
27
}
28
29
var terminalAttachCmd = &cobra.Command{
30
Use: "attach <alias>",
31
Short: "attaches to a terminal",
32
Args: cobra.MaximumNArgs(1),
33
Run: func(cmd *cobra.Command, args []string) {
34
var (
35
alias string
36
client = api.NewTerminalServiceClient(dialSupervisor())
37
)
38
if len(args) == 0 {
39
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
40
defer cancel()
41
resp, err := client.List(ctx, &api.ListTerminalsRequest{})
42
if err != nil {
43
log.WithError(err).Fatal("cannot list terminals")
44
}
45
if len(resp.Terminals) == 0 {
46
log.Fatal("no terminal available")
47
}
48
if len(resp.Terminals) > 1 {
49
fmt.Fprintln(os.Stderr, "More than one terminal, please choose explicitly:")
50
for _, r := range resp.Terminals {
51
fmt.Fprintf(os.Stderr, "\t%s\n", r.Alias)
52
}
53
os.Exit(1)
54
}
55
56
alias = resp.Terminals[0].Alias
57
} else {
58
alias = args[0]
59
}
60
61
attachToTerminal(
62
context.Background(),
63
client,
64
alias,
65
attachToTerminalOpts{
66
ForceResize: terminalAttachOpts.ForceResize,
67
Interactive: terminalAttachOpts.Interactive,
68
},
69
)
70
},
71
}
72
73
type attachToTerminalOpts struct {
74
Interactive bool
75
ForceResize bool
76
Token string
77
}
78
79
func attachToTerminal(ctx context.Context, client api.TerminalServiceClient, alias string, opts attachToTerminalOpts) {
80
// Copy to stdout/stderr
81
listen, err := client.Listen(ctx, &api.ListenTerminalRequest{
82
Alias: alias,
83
})
84
if err != nil {
85
log.WithError(err).Fatal("cannot attach to terminal")
86
}
87
var exitCode int
88
errchan := make(chan error, 5)
89
go func() {
90
for {
91
resp, err := listen.Recv()
92
if err != nil {
93
errchan <- err
94
}
95
os.Stdout.Write(resp.GetData())
96
terminalExitCode := resp.GetExitCode()
97
if terminalExitCode > 0 {
98
exitCode = int(terminalExitCode)
99
}
100
}
101
}()
102
103
// Set stdin in raw mode.
104
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
105
if err != nil {
106
panic(err)
107
}
108
defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
109
110
if opts.Interactive {
111
// Handle pty size.
112
ch := make(chan os.Signal, 1)
113
signal.Notify(ch, syscall.SIGWINCH)
114
go func() {
115
for range ch {
116
size, err := pty.GetsizeFull(os.Stdin)
117
if err != nil {
118
log.WithError(err).Error("cannot determine stdin's terminal size")
119
continue
120
}
121
122
req := &api.SetTerminalSizeRequest{
123
Alias: alias,
124
Size: &api.TerminalSize{
125
Cols: uint32(size.Cols),
126
Rows: uint32(size.Rows),
127
WidthPx: uint32(size.X),
128
HeightPx: uint32(size.Y),
129
},
130
}
131
132
var expectResize bool
133
if opts.ForceResize {
134
req.Priority = &api.SetTerminalSizeRequest_Force{Force: true}
135
expectResize = true
136
} else if opts.Token != "" {
137
req.Priority = &api.SetTerminalSizeRequest_Token{Token: opts.Token}
138
expectResize = true
139
}
140
141
_, err = client.SetSize(ctx, req)
142
if err != nil && expectResize {
143
log.WithError(err).Error("cannot set terminal size")
144
continue
145
}
146
}
147
}()
148
ch <- syscall.SIGWINCH // Initial resize.
149
150
// Copy stdin to the pty and the pty to stdout.
151
go func() {
152
buf := make([]byte, 32*1024)
153
for {
154
n, err := os.Stdin.Read(buf)
155
if n > 0 {
156
_, serr := client.Write(ctx, &api.WriteTerminalRequest{Alias: alias, Stdin: buf[:n]})
157
if serr != nil {
158
errchan <- err
159
return
160
}
161
}
162
if err != nil {
163
errchan <- err
164
return
165
}
166
}
167
}()
168
}
169
170
// wait indefinitely
171
stopch := make(chan os.Signal, 1)
172
signal.Notify(stopch, syscall.SIGTERM|syscall.SIGINT)
173
select {
174
case err := <-errchan:
175
if err != io.EOF {
176
log.WithError(err).Error("error")
177
} else {
178
os.Exit(exitCode)
179
}
180
case <-stopch:
181
}
182
}
183
184
func init() {
185
terminalCmd.AddCommand(terminalAttachCmd)
186
187
terminalAttachCmd.Flags().BoolVarP(&terminalAttachOpts.Interactive, "internactive", "i", false, "assume control over the terminal")
188
terminalAttachCmd.Flags().BoolVarP(&terminalAttachOpts.ForceResize, "force-resize", "r", false, "force this terminal's size irregardless of other clients")
189
}
190
191