Skip to content

Commit 829b3b6

Browse files
committed
perf(ssh): record session using writer adpter, and queue processing
1 parent 0c293db commit 829b3b6

File tree

1 file changed

+65
-53
lines changed

1 file changed

+65
-53
lines changed

ssh/server/channels/utils.go

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package channels
22

33
import (
4-
"bytes"
54
"io"
65
"sync"
76

@@ -14,6 +13,69 @@ import (
1413
gossh "golang.org/x/crypto/ssh"
1514
)
1615

16+
type Recorder struct {
17+
queue chan string
18+
channel gossh.Channel
19+
}
20+
21+
func NewRecorder(sess *session.Session, camera *session.Camera, channel gossh.Channel) io.WriteCloser {
22+
queue := make(chan string, 100)
23+
go func() {
24+
recording := true
25+
26+
for {
27+
msg, ok := <-queue
28+
if !ok {
29+
return
30+
}
31+
32+
if !recording {
33+
continue
34+
}
35+
36+
if err := camera.WriteFrame(&models.SessionRecorded{ //nolint:errcheck
37+
UID: sess.UID,
38+
Namespace: sess.Lookup["domain"],
39+
Message: msg,
40+
Width: int(sess.Pty.Columns),
41+
Height: int(sess.Pty.Rows),
42+
}); err != nil {
43+
log.WithError(err).
44+
WithFields(log.Fields{"session": sess.UID, "sshid": sess.SSHID}).
45+
Warning("failed to send the session frame to record")
46+
47+
recording = false
48+
}
49+
}
50+
}()
51+
52+
return &Recorder{
53+
queue: queue,
54+
channel: channel,
55+
}
56+
}
57+
58+
func (c *Recorder) record(msg string) {
59+
c.queue <- msg
60+
}
61+
62+
func (c *Recorder) Write(data []byte) (int, error) {
63+
read, err := c.channel.Write(data)
64+
if err != nil {
65+
return read, err
66+
}
67+
68+
go c.record(string(data))
69+
70+
return read, nil
71+
}
72+
73+
func (c *Recorder) Close() error {
74+
close(c.queue)
75+
76+
return c.channel.CloseWrite()
77+
}
78+
1779
// pipe pipes data between client and agent, and vise versa, recoding each frame when ShellHub instance are Cloud or
1880
// Enterprise.
1981
func pipe(ctx gliderssh.Context, sess *session.Session, client gossh.Channel, agent gossh.Channel) {
@@ -42,69 +104,19 @@ func pipe(ctx gliderssh.Context, sess *session.Session, client gossh.Channel, ag
42104
defer wg.Done()
43105
defer client.CloseWrite() //nolint:errcheck
44106

45-
// NOTE: As the copy required to record the session seem to be inefficient, if we don't have a record URL
46-
// defined, we use an [io.Copy] for the data piping between agent and client.
47107
recordURL := ctx.Value("RECORD_URL").(string)
48108
if (envs.IsEnterprise() || envs.IsCloud()) && recordURL != "" {
49-
// NOTE: Recoding variable is used to control if the frames will be recorded. If something wrong happens in
50-
// this process, to spare resources, we don't send frames anymore for this session.
51-
recording := true
52-
53109
camera, err := sess.Record(ctx, recordURL)
54110
if err != nil {
55111
log.WithError(err).
56112
WithFields(log.Fields{"session": sess.UID, "sshid": sess.SSHID, "record_url": recordURL}).
57113
Warning("failed to connect to session record endpoint")
58-
59-
recording = false
60114
}
61115

62116
defer camera.Close()
63117

64-
buffer := make([]byte, 1024)
65-
for {
66-
read, err := a.Read(buffer)
67-
// The occurrence of io.EOF is expected when the connection ends.
68-
// This indicates that we have reached the end of the input stream, and we need
69-
// to break out of the loop to handle the termination of the connection
70-
if err == io.EOF {
71-
break
72-
}
73-
// Unlike io.EOF, when 'err' is simply not nil, it signifies an unexpected error,
74-
// and we need to log to handle it appropriately.
75-
if err != nil {
76-
log.WithError(err).
77-
WithFields(log.Fields{"session": sess.UID, "sshid": sess.SSHID}).
78-
Warning("failed to read from stdout in pty client")
79-
80-
break
81-
}
82-
83-
if _, err = io.Copy(client, bytes.NewReader(buffer[:read])); err != nil && err != io.EOF {
84-
log.WithError(err).
85-
WithFields(log.Fields{"session": sess.UID, "sshid": sess.SSHID}).
86-
Warning("failed to copy from stdout in pty client")
87-
88-
break
89-
}
90-
91-
if recording {
92-
if err := camera.WriteFrame(&models.SessionRecorded{ //nolint:errcheck
93-
UID: sess.UID,
94-
Namespace: sess.Lookup["domain"],
95-
Message: string(buffer[:read]),
96-
Width: int(sess.Pty.Columns),
97-
Height: int(sess.Pty.Rows),
98-
}); err != nil {
99-
log.WithError(err).
100-
WithFields(log.Fields{"session": sess.UID, "sshid": sess.SSHID}).
101-
Warning("failed to send the session frame to record")
102-
103-
recording = false
104-
105-
continue
106-
}
107-
}
118+
if _, err := io.Copy(NewRecorder(sess, camera, client), a); err != nil && err != io.EOF {
119+
log.WithError(err).Error("failed on coping data from client to agent")
108120
}
109121
} else {
110122
if _, err := io.Copy(client, a); err != nil && err != io.EOF {

0 commit comments

Comments
 (0)