forked from elisescu/tty-share
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
209 lines (177 loc) · 4.63 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/signal"
"sync"
"sync/atomic"
"syscall"
"github.com/elisescu/tty-share/server"
"github.com/gorilla/websocket"
"github.com/moby/term"
log "github.com/sirupsen/logrus"
)
type ttyShareClient struct {
url string
wsConn *websocket.Conn
detachKeys string
wcChan chan os.Signal
ioFlagAtomic uint32 // used with atomic
winSizes struct {
thisW uint16
thisH uint16
remoteW uint16
remoteH uint16
}
winSizesMutex sync.Mutex
}
func newTtyShareClient(url string, detachKeys string) *ttyShareClient {
return &ttyShareClient{
url: url,
wsConn: nil,
detachKeys: detachKeys,
wcChan: make(chan os.Signal, 1),
ioFlagAtomic: 1,
}
}
func clearScreen() {
fmt.Fprintf(os.Stdout, "\033[H\033[2J")
}
type keyListener struct {
wrappedReader io.Reader
ioFlagAtomicP *uint32
}
func (kl *keyListener) Read(data []byte) (n int, err error) {
n, err = kl.wrappedReader.Read(data)
if _, ok := err.(term.EscapeError); ok {
log.Debug("Escape code detected.")
}
// If we are not supposed to do any IO, then return 0 bytes read. This happens the local
// window is smaller than the remote one
if atomic.LoadUint32(kl.ioFlagAtomicP) == 0 {
return 0, err
}
return
}
func (c *ttyShareClient) updateAndDecideStdoutMuted() {
log.Infof("This window: %dx%d. Remote window: %dx%d", c.winSizes.thisW, c.winSizes.thisH, c.winSizes.remoteW, c.winSizes.remoteH)
if c.winSizes.thisH < c.winSizes.remoteH || c.winSizes.thisW < c.winSizes.remoteW {
atomic.StoreUint32(&c.ioFlagAtomic, 0)
clearScreen()
messageFormat := "\n\rYour window is smaller than the remote window. Please resize or press <C-o C-c> to detach.\n\r\tRemote window: %dx%d \n\r\tYour window: %dx%d \n\r"
fmt.Printf(messageFormat, c.winSizes.remoteW, c.winSizes.remoteH, c.winSizes.thisW, c.winSizes.thisH)
} else {
if atomic.LoadUint32(&c.ioFlagAtomic) == 0 { // clear the screen when changing back to "write"
// TODO: notify the remote side to "refresh" the content.
clearScreen()
}
atomic.StoreUint32(&c.ioFlagAtomic, 1)
}
}
func (c *ttyShareClient) updateThisWinSize() {
size, err := term.GetWinsize(os.Stdin.Fd())
if err == nil {
c.winSizesMutex.Lock()
c.winSizes.thisW = size.Width
c.winSizes.thisH = size.Height
c.winSizesMutex.Unlock()
}
}
func (c *ttyShareClient) Run() (err error) {
log.Debugf("Connecting as a client to %s ..", c.url)
resp, err := http.Get(c.url)
if err != nil {
return
}
// Get the path of the websockts route from the header
wsPath := resp.Header.Get("TTYSHARE-WSPATH")
// Build the WS URL from the host part of the given http URL and the wsPath
httpURL, err := url.Parse(c.url)
if err != nil {
return
}
wsScheme := "ws"
if httpURL.Scheme == "https" {
wsScheme = "wss"
}
wsURL := wsScheme + "://" + httpURL.Host + wsPath
log.Debugf("Built the WS URL from the headers: %s", wsURL)
c.wsConn, _, err = websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
return
}
detachBytes, err := term.ToBytes(c.detachKeys)
if err != nil {
log.Errorf("Invalid dettaching keys: %s", c.detachKeys)
return
}
state, err := term.MakeRaw(os.Stdin.Fd())
defer term.RestoreTerminal(os.Stdin.Fd(), state)
clearScreen()
protoWS := server.NewTTYProtocolWSLocked(c.wsConn)
monitorWinChanges := func() {
// start monitoring the size of the terminal
signal.Notify(c.wcChan, syscall.SIGWINCH)
for {
select {
case <-c.wcChan:
c.updateThisWinSize()
c.updateAndDecideStdoutMuted()
protoWS.SetWinSize(int(c.winSizes.thisW), int(c.winSizes.thisH))
}
}
}
readLoop := func() {
var err error
for {
err = protoWS.ReadAndHandle(
// onWrite
func(data []byte) {
if atomic.LoadUint32(&c.ioFlagAtomic) != 0 {
os.Stdout.Write(data)
}
},
// onWindowSize
func(cols, rows int) {
c.winSizesMutex.Lock()
c.winSizes.remoteW = uint16(cols)
c.winSizes.remoteH = uint16(rows)
c.winSizesMutex.Unlock()
c.updateThisWinSize()
c.updateAndDecideStdoutMuted()
},
)
if err != nil {
log.Errorf("Error parsing remote message: %s", err.Error())
if err == io.EOF {
// Remote WS connection closed
return
}
}
}
}
writeLoop := func() {
kl := &keyListener{
wrappedReader: term.NewEscapeProxy(os.Stdin, detachBytes),
ioFlagAtomicP: &c.ioFlagAtomic,
}
_, err := io.Copy(protoWS, kl)
if err != nil {
log.Debugf("Connection closed: %s", err.Error())
c.Stop()
return
}
}
go monitorWinChanges()
go writeLoop()
readLoop()
clearScreen()
return
}
func (c *ttyShareClient) Stop() {
c.wsConn.Close()
signal.Stop(c.wcChan)
}