Skip to content

Commit c331fdc

Browse files
committed
pushing possible fix for dying connections using ping and pong mechanism
1 parent 44f87bf commit c331fdc

4 files changed

Lines changed: 45 additions & 11 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Configurations can be set via flags or environment variables. To view available
8282
| Arguments | `--arguments` | `ARGUMENTS` | `"-l"` | Comma delimited list of arguments that should be passed to the target binary |
8383
| Command | `--command` | `COMMAND` | `"/bin/bash"` | Absolute path to the binary to run |
8484
| Connection error limit | `--connection-error-limit` | `CONNECTION_ERROR_LIMIT` | `10` | Number of times a connection should be re-attempted by the server to the XTerm.js frontend before the connection is considered dead and shut down |
85+
| Keepalive ping timeout | `--keepalive-ping-timeout` | `KEEPALIVE_PING_TIMEOUT` | `20` | Maximum duration in seconds between a ping and pong message to tolerate |
8586
| Maximum buffer size in bytes | `--max-buffer-size-bytes` | `MAX_BUFFER_SIZE_BYTES` | `512` | Maximum length of input from the browser terminal |
8687
| Log format | `--log-format` | `LOG_FORMAT` | `"text"` | Format with which to output logs, one of `"json"` or `"text"` |
8788
| Log level | `--log-level` | `LOG_LEVEL` | `"debug"` | Minimum level of logs to output, one of `"trace"`, `"debug"`, `"info"`, `"warn"`, `"error"` |

cmd/cloudshell/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ var conf = config.Map{
2929
Usage: "number of times a connection should be re-attempted before it's considered dead",
3030
Shorthand: "l",
3131
},
32+
"keepalive-ping-timeout": &config.Int{
33+
Default: 20,
34+
Usage: "maximum duration in seconds between a ping message and its response to tolerate",
35+
Shorthand: "k",
36+
},
3237
"max-buffer-size-bytes": &config.Int{
3338
Default: 512,
3439
Usage: "maximum length of input from terminal",

cmd/cloudshell/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func runE(_ *cobra.Command, _ []string) error {
4141
connectionErrorLimit := conf.GetInt("connection-error-limit")
4242
arguments := conf.GetStringSlice("arguments")
4343
allowedHostnames := conf.GetStringSlice("allowed-hostnames")
44+
keepalivePingTimeout := time.Duration(conf.GetInt("keepalive-ping-timeout")) * time.Second
4445
maxBufferSizeBytes := conf.GetInt("max-buffer-size-bytes")
4546
pathLiveness := conf.GetString("path-liveness")
4647
pathMetrics := conf.GetString("path-metrics")
@@ -64,6 +65,7 @@ func runE(_ *cobra.Command, _ []string) error {
6465

6566
log.Infof("allowed hosts : ['%s']", strings.Join(allowedHostnames, "', '"))
6667
log.Infof("connection error limit: %v", connectionErrorLimit)
68+
log.Infof("keepalive ping timeout: %v", keepalivePingTimeout)
6769
log.Infof("max buffer size : %v bytes", maxBufferSizeBytes)
6870
log.Infof("server address : '%s' ", serverAddress)
6971
log.Infof("server port : %v", serverPort)
@@ -86,7 +88,8 @@ func runE(_ *cobra.Command, _ []string) error {
8688
createRequestLog(r, map[string]interface{}{"connection_uuid": connectionUUID}).Infof("created logger for connection '%s'", connectionUUID)
8789
return createRequestLog(nil, map[string]interface{}{"connection_uuid": connectionUUID})
8890
},
89-
MaxBufferSizeBytes: maxBufferSizeBytes,
91+
KeepalivePingTimeout: keepalivePingTimeout,
92+
MaxBufferSizeBytes: maxBufferSizeBytes,
9093
}
9194
router.HandleFunc(pathXTermJS, xtermjs.GetHandler(xtermjsHandlerOptions))
9295

pkg/xtermjs/handler_websocket.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os/exec"
1111
"strings"
1212
"sync"
13+
"time"
1314

1415
"github.com/creack/pty"
1516
"github.com/google/uuid"
@@ -32,8 +33,11 @@ type HandlerOpts struct {
3233
// CreateLogger when specified should return a logger that the handler will use.
3334
// The string argument being passed in will be a unique identifier for the
3435
// current connection. When not specified, logs will be sent to stdout
35-
CreateLogger func(string, *http.Request) Logger
36-
MaxBufferSizeBytes int
36+
CreateLogger func(string, *http.Request) Logger
37+
// KeepalivePingTimeout defines the maximum duration between which a ping and pong
38+
// cycle should be tolerated, beyond this the connection should be deemed dead
39+
KeepalivePingTimeout time.Duration
40+
MaxBufferSizeBytes int
3741
}
3842

3943
func GetHandler(opts HandlerOpts) func(http.ResponseWriter, *http.Request) {
@@ -43,6 +47,10 @@ func GetHandler(opts HandlerOpts) func(http.ResponseWriter, *http.Request) {
4347
connectionErrorLimit = DefaultConnectionErrorLimit
4448
}
4549
maxBufferSizeBytes := opts.MaxBufferSizeBytes
50+
keepalivePingTimeout := opts.KeepalivePingTimeout
51+
if keepalivePingTimeout <= time.Second {
52+
keepalivePingTimeout = 20 * time.Second
53+
}
4654

4755
connectionUUID, err := uuid.NewUUID()
4856
if err != nil {
@@ -98,6 +106,28 @@ func GetHandler(opts HandlerOpts) func(http.ResponseWriter, *http.Request) {
98106
var waiter sync.WaitGroup
99107
waiter.Add(1)
100108

109+
// this is a keep-alive loop that ensures connection does not hang-up itself
110+
lastPongTime := time.Now()
111+
connection.SetPongHandler(func(msg string) error {
112+
lastPongTime = time.Now()
113+
return nil
114+
})
115+
go func() {
116+
for {
117+
if err := connection.WriteMessage(websocket.PingMessage, []byte("keepalive")); err != nil {
118+
clog.Warn("failed to write ping message")
119+
return
120+
}
121+
time.Sleep(keepalivePingTimeout / 2)
122+
if time.Now().Sub(lastPongTime) > keepalivePingTimeout {
123+
clog.Warn("failed to get response from ping, triggering disconnect now...")
124+
waiter.Done()
125+
return
126+
}
127+
clog.Debug("received response from ping successfully")
128+
}
129+
}()
130+
101131
// tty >> xterm.js
102132
go func() {
103133
errorCounter := 0
@@ -133,20 +163,15 @@ func GetHandler(opts HandlerOpts) func(http.ResponseWriter, *http.Request) {
133163
go func() {
134164
for {
135165
// data processing
136-
messageType, reader, err := connection.NextReader()
166+
messageType, data, err := connection.ReadMessage()
137167
if err != nil {
138168
if !connectionClosed {
139169
clog.Warnf("failed to get next reader: %s", err)
140170
}
141171
return
142172
}
143-
dataBuffer := make([]byte, maxBufferSizeBytes)
144-
dataLength, err := reader.Read(dataBuffer)
145-
if err != nil {
146-
clog.Warn("failed to get data from buffer: %s", err)
147-
return
148-
}
149-
dataBuffer = bytes.Trim(dataBuffer, "\x00")
173+
dataLength := len(data)
174+
dataBuffer := bytes.Trim(data, "\x00")
150175
dataType, ok := WebsocketMessageType[messageType]
151176
if !ok {
152177
dataType = "uunknown"

0 commit comments

Comments
 (0)