Skip to content

Commit f4458d6

Browse files
committed
fix(agent): Prevent loss of SSH PTY command output
Non-ideal fix for PTY output loss because it always adds a delay of 100ms. Completely fixes #6656
1 parent b38d1ed commit f4458d6

File tree

2 files changed

+18
-10
lines changed

2 files changed

+18
-10
lines changed

agent/agent.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,18 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
11041104
var wg sync.WaitGroup
11051105
defer func() {
11061106
defer wg.Wait()
1107+
1108+
// If we call Close() before the output is read, the output
1109+
// will be lost. We set a deadline on the read so that we can
1110+
// wait for the output to be read before closing the PTY.
1111+
// OpenSSH also uses a 100ms timeout for reading from the PTY.
1112+
if dlErr := ptty.Output().Reader.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); dlErr != nil {
1113+
a.logger.Warn(ctx, "failed to set read deadline, pty output may be lost", slog.Error(dlErr))
1114+
} else {
1115+
// If we successfully set the deadline, we can immediately
1116+
// wait for the output copy goroutine to exit.
1117+
wg.Wait()
1118+
}
11071119
closeErr := ptty.Close()
11081120
if closeErr != nil {
11091121
a.logger.Warn(ctx, "failed to close tty", slog.Error(closeErr))
@@ -1131,8 +1143,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
11311143
// output being lost. To avoid this, we wait for the output copy to
11321144
// start before waiting for the command to exit. This ensures that the
11331145
// output copy goroutine will be scheduled before calling close on the
1134-
// pty. There is still a risk of data loss if a command produces a lot
1135-
// of output, see TestAgent_Session_TTY_HugeOutputIsNotLost (skipped).
1146+
// pty. This is a safety-net in case SetReadDeadline doesn't work.
11361147
outputCopyStarted := make(chan struct{})
11371148
ptyOutput := func() io.Reader {
11381149
defer close(outputCopyStarted)

agent/agent_test.go

+5-8
Original file line numberDiff line numberDiff line change
@@ -410,14 +410,11 @@ func TestAgent_Session_TTY_HugeOutputIsNotLost(t *testing.T) {
410410
// it seems like it could be either.
411411
t.Skip("ConPTY appears to be inconsistent on Windows.")
412412
}
413-
t.Skip("This test proves we have a bug where parts of large output on a PTY can be lost after the command exits, skipped to avoid test failures.")
414-
415-
// This test is here to prevent prove we have a bug where quickly executing
416-
// commands (with TTY) don't flush their output to the SSH session. This is
417-
// due to the pty being closed before all the output has been copied, but
418-
// protecting against this requires a non-trivial rewrite of the output
419-
// processing (or figuring out a way to put the pty in a mode where this
420-
// does not happen).
413+
414+
// This test is here to prevent regressions where the PTY for commands is
415+
// closed before the buffer is consumed.
416+
//
417+
// See: https://github.com/coder/coder/issues/6656
421418
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
422419
defer cancel()
423420
//nolint:dogsled

0 commit comments

Comments
 (0)