mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
138 lines
3.2 KiB
Go
138 lines
3.2 KiB
Go
package reconnectingpty
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/sloghuman"
|
|
"github.com/coder/coder/coderd/tracing"
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/loadtest/harness"
|
|
"github.com/coder/coder/loadtest/loadtestutil"
|
|
)
|
|
|
|
type Runner struct {
|
|
client *codersdk.Client
|
|
cfg Config
|
|
}
|
|
|
|
var _ harness.Runnable = &Runner{}
|
|
|
|
func NewRunner(client *codersdk.Client, cfg Config) *Runner {
|
|
return &Runner{
|
|
client: client,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
// Run implements Runnable.
|
|
func (r *Runner) Run(ctx context.Context, _ string, logs io.Writer) error {
|
|
ctx, span := tracing.StartSpan(ctx)
|
|
defer span.End()
|
|
|
|
logs = loadtestutil.NewSyncWriter(logs)
|
|
logger := slog.Make(sloghuman.Sink(logs)).Leveled(slog.LevelDebug)
|
|
r.client.Logger = logger
|
|
r.client.LogBodies = true
|
|
|
|
var (
|
|
id = r.cfg.Init.ID
|
|
width = r.cfg.Init.Width
|
|
height = r.cfg.Init.Height
|
|
)
|
|
if id == uuid.Nil {
|
|
id = uuid.New()
|
|
}
|
|
if width == 0 {
|
|
width = DefaultWidth
|
|
}
|
|
if height == 0 {
|
|
height = DefaultHeight
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(logs, "Opening reconnecting PTY connection to agent via coderd...")
|
|
_, _ = fmt.Fprintf(logs, "\tID: %s\n", id.String())
|
|
_, _ = fmt.Fprintf(logs, "\tWidth: %d\n", width)
|
|
_, _ = fmt.Fprintf(logs, "\tHeight: %d\n", height)
|
|
_, _ = fmt.Fprintf(logs, "\tCommand: %q\n\n", r.cfg.Init.Command)
|
|
|
|
conn, err := r.client.WorkspaceAgentReconnectingPTY(ctx, r.cfg.AgentID, id, width, height, r.cfg.Init.Command)
|
|
if err != nil {
|
|
return xerrors.Errorf("open reconnecting PTY: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
var (
|
|
copyTimeout = r.cfg.Timeout
|
|
copyOutput = io.Discard
|
|
)
|
|
if copyTimeout == 0 {
|
|
copyTimeout = DefaultTimeout
|
|
}
|
|
if r.cfg.LogOutput {
|
|
_, _ = fmt.Fprintln(logs, "Output:")
|
|
copyOutput = logs
|
|
}
|
|
|
|
copyCtx, copyCancel := context.WithTimeout(ctx, time.Duration(copyTimeout))
|
|
matched, err := copyContext(copyCtx, copyOutput, conn, r.cfg.ExpectOutput)
|
|
copyCancel()
|
|
if r.cfg.ExpectTimeout {
|
|
if err == nil {
|
|
return xerrors.Errorf("expected timeout, but the command exited successfully")
|
|
}
|
|
if !xerrors.Is(err, context.DeadlineExceeded) {
|
|
return xerrors.Errorf("expected timeout, but got a different error: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return xerrors.Errorf("copy context: %w", err)
|
|
}
|
|
if !matched {
|
|
return xerrors.Errorf("expected string %q not found in output", r.cfg.ExpectOutput)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyContext(ctx context.Context, dst io.Writer, src io.Reader, expectOutput string) (bool, error) {
|
|
var (
|
|
copyErr = make(chan error)
|
|
matched = expectOutput == ""
|
|
)
|
|
go func() {
|
|
defer close(copyErr)
|
|
|
|
scanner := bufio.NewScanner(src)
|
|
for scanner.Scan() {
|
|
if expectOutput != "" && strings.Contains(scanner.Text(), expectOutput) {
|
|
matched = true
|
|
}
|
|
|
|
_, err := dst.Write([]byte("\t" + scanner.Text() + "\n"))
|
|
if err != nil {
|
|
copyErr <- xerrors.Errorf("write to logs: %w", err)
|
|
return
|
|
}
|
|
}
|
|
if scanner.Err() != nil {
|
|
copyErr <- xerrors.Errorf("read from reconnecting PTY: %w", scanner.Err())
|
|
return
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return matched, ctx.Err()
|
|
case err := <-copyErr:
|
|
return matched, err
|
|
}
|
|
}
|