@@ -1104,6 +1104,18 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
1104
1104
var wg sync.WaitGroup
1105
1105
defer func () {
1106
1106
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
+ }
1107
1119
closeErr := ptty .Close ()
1108
1120
if closeErr != nil {
1109
1121
a .logger .Warn (ctx , "failed to close tty" , slog .Error (closeErr ))
@@ -1131,8 +1143,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
1131
1143
// output being lost. To avoid this, we wait for the output copy to
1132
1144
// start before waiting for the command to exit. This ensures that the
1133
1145
// 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.
1136
1147
outputCopyStarted := make (chan struct {})
1137
1148
ptyOutput := func () io.Reader {
1138
1149
defer close (outputCopyStarted )
0 commit comments