-
Notifications
You must be signed in to change notification settings - Fork 894
refactor: PTY & SSH #7100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: PTY & SSH #7100
Changes from 1 commit
0075d7d
a491d4f
2cf357a
28c0646
872e357
3f21e30
e83ff6e
b610579
90bfe94
d6e131c
2c9c6ef
e39e885
8ec3d1f
df424e6
50e3fec
c09083e
439107d
c6a3229
aa94546
0f07cb9
eaabb3a
50060d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Signed-off-by: Spike Curtis <spike@coder.com>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -141,6 +141,10 @@ func (p *ptyWindows) Close() error { | |
} | ||
p.closed = true | ||
|
||
// if we are running a command in the PTY, the corresponding *windowsProcess | ||
// may have already closed the PseudoConsole when the command exited, so that | ||
// output reads can get to EOF. In that case, we don't need to close it | ||
// again here. | ||
if p.console != windows.InvalidHandle { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assigned both here and in waitInternal, probably needs protection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protected by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right, the |
||
ret, _, err := procClosePseudoConsole.Call(uintptr(p.console)) | ||
if ret < 0 { | ||
|
@@ -169,9 +173,11 @@ func (p *windowsProcess) waitInternal() { | |
defer p.pw.closeMutex.Unlock() | ||
if p.pw.console != windows.InvalidHandle { | ||
ret, _, err := procClosePseudoConsole.Call(uintptr(p.pw.console)) | ||
if ret < 0 { | ||
// not much we can do here... | ||
panic(err) | ||
if ret < 0 && p.cmdErr == nil { | ||
// if we already have an error from the command, prefer that error | ||
// but if the command succeeded and closing the PseudoConsole fails | ||
// then record that errror so that we have a chance to see it | ||
p.cmdErr = err | ||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be nice to log the error whether it's kept or discarded There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice, but I don't have a logger, and I'd have to change all the init signatures to get one, so I decided on this. Another alternative I considered is panicking---something pretty messed up at the OS level is happening if we get an error here. We're pretty much guaranteed to crash the agent if we panic, so it's likely we'd get told about it. |
||
} | ||
p.pw.console = windows.InvalidHandle | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,158 @@ | ||||||||||||
package pty_test | ||||||||||||
|
||||||||||||
import ( | ||||||||||||
"bytes" | ||||||||||||
"context" | ||||||||||||
"fmt" | ||||||||||||
"io" | ||||||||||||
"os/exec" | ||||||||||||
"strings" | ||||||||||||
"testing" | ||||||||||||
"time" | ||||||||||||
|
||||||||||||
"github.com/coder/coder/pty" | ||||||||||||
"github.com/hinshun/vt10x" | ||||||||||||
"github.com/stretchr/testify/assert" | ||||||||||||
"github.com/stretchr/testify/require" | ||||||||||||
) | ||||||||||||
|
||||||||||||
// Test_Start_copy tests that we can use io.Copy() on command output | ||||||||||||
// without deadlocking. | ||||||||||||
func Test_Start_copy(t *testing.T) { | ||||||||||||
t.Parallel() | ||||||||||||
|
||||||||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) | ||||||||||||
defer cancel() | ||||||||||||
|
||||||||||||
pc, cmd, err := pty.Start(exec.CommandContext(ctx, cmdEcho, argEcho...)) | ||||||||||||
require.NoError(t, err) | ||||||||||||
b := &bytes.Buffer{} | ||||||||||||
readDone := make(chan error) | ||||||||||||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
go func() { | ||||||||||||
_, err := io.Copy(b, pc.OutputReader()) | ||||||||||||
readDone <- err | ||||||||||||
}() | ||||||||||||
|
||||||||||||
select { | ||||||||||||
case err := <-readDone: | ||||||||||||
require.NoError(t, err) | ||||||||||||
case <-ctx.Done(): | ||||||||||||
t.Error("read timed out") | ||||||||||||
} | ||||||||||||
assert.Contains(t, b.String(), "test") | ||||||||||||
|
||||||||||||
cmdDone := make(chan error) | ||||||||||||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
go func() { | ||||||||||||
cmdDone <- cmd.Wait() | ||||||||||||
}() | ||||||||||||
|
||||||||||||
select { | ||||||||||||
case err := <-cmdDone: | ||||||||||||
require.NoError(t, err) | ||||||||||||
case <-ctx.Done(): | ||||||||||||
t.Error("cmd.Wait() timed out") | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
// Test_Start_truncation tests that we can read command ouput without truncation | ||||||||||||
// even after the command has exited. | ||||||||||||
func Test_Start_trucation(t *testing.T) { | ||||||||||||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
t.Parallel() | ||||||||||||
|
||||||||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1000) | ||||||||||||
defer cancel() | ||||||||||||
|
||||||||||||
pc, cmd, err := pty.Start(exec.CommandContext(ctx, cmdCount, argCount...)) | ||||||||||||
|
||||||||||||
require.NoError(t, err) | ||||||||||||
readDone := make(chan struct{}) | ||||||||||||
go func() { | ||||||||||||
defer close(readDone) | ||||||||||||
// avoid buffered IO so that we can precisely control how many bytes to read. | ||||||||||||
n := 1 | ||||||||||||
for n < countEnd-25 { | ||||||||||||
want := fmt.Sprintf("%d", n) | ||||||||||||
err := readUntil(ctx, t, want, pc.OutputReader()) | ||||||||||||
require.NoError(t, err, "want: %s", want) | ||||||||||||
n++ | ||||||||||||
} | ||||||||||||
}() | ||||||||||||
|
||||||||||||
select { | ||||||||||||
case <-readDone: | ||||||||||||
// OK! | ||||||||||||
case <-ctx.Done(): | ||||||||||||
t.Error("read timed out") | ||||||||||||
} | ||||||||||||
|
||||||||||||
cmdDone := make(chan error) | ||||||||||||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
go func() { | ||||||||||||
cmdDone <- cmd.Wait() | ||||||||||||
}() | ||||||||||||
|
||||||||||||
select { | ||||||||||||
case err := <-cmdDone: | ||||||||||||
require.NoError(t, err) | ||||||||||||
case <-ctx.Done(): | ||||||||||||
t.Error("cmd.Wait() timed out") | ||||||||||||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
} | ||||||||||||
|
||||||||||||
// do our final 25 reads, to make sure the output wasn't lost | ||||||||||||
readDone = make(chan struct{}) | ||||||||||||
go func() { | ||||||||||||
defer close(readDone) | ||||||||||||
// avoid buffered IO so that we can precisely control how many bytes to read. | ||||||||||||
n := countEnd - 25 | ||||||||||||
for n <= countEnd { | ||||||||||||
want := fmt.Sprintf("%d", n) | ||||||||||||
err := readUntil(ctx, t, want, pc.OutputReader()) | ||||||||||||
require.NoError(t, err, "want: %s", want) | ||||||||||||
n++ | ||||||||||||
} | ||||||||||||
// ensure we still get to EOF | ||||||||||||
endB := &bytes.Buffer{} | ||||||||||||
_, err := io.Copy(endB, pc.OutputReader()) | ||||||||||||
require.NoError(t, err) | ||||||||||||
|
||||||||||||
}() | ||||||||||||
|
||||||||||||
select { | ||||||||||||
case <-readDone: | ||||||||||||
// OK! | ||||||||||||
case <-ctx.Done(): | ||||||||||||
t.Error("read timed out") | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
// readUntil reads one byte at a time until we either see the string we want, or the context expires | ||||||||||||
func readUntil(ctx context.Context, t *testing.T, want string, r io.Reader) error { | ||||||||||||
// output can contain virtual terminal sequences, so we need to parse these | ||||||||||||
// to correctly interpret getting what we want. | ||||||||||||
term := vt10x.New(vt10x.WithSize(80, 80)) | ||||||||||||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
readErrs := make(chan error, 1) | ||||||||||||
for { | ||||||||||||
b := make([]byte, 1) | ||||||||||||
go func() { | ||||||||||||
_, err := r.Read(b) | ||||||||||||
readErrs <- err | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is needed. The whole purpose of this little goroutine is to allow me to call |
||||||||||||
}() | ||||||||||||
select { | ||||||||||||
case err := <-readErrs: | ||||||||||||
if err != nil { | ||||||||||||
t.Logf("err: %v\ngot: %v", err, term) | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
term.Write(b) | ||||||||||||
case <-ctx.Done(): | ||||||||||||
return ctx.Err() | ||||||||||||
} | ||||||||||||
got := term.String() | ||||||||||||
lines := strings.Split(got, "\n") | ||||||||||||
for _, line := range lines { | ||||||||||||
if strings.TrimSpace(line) == want { | ||||||||||||
t.Logf("want: %v\n got:%v", want, line) | ||||||||||||
return nil | ||||||||||||
} | ||||||||||||
} | ||||||||||||
spikecurtis marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
} | ||||||||||||
} |
Uh oh!
There was an error while loading. Please reload this page.