AgentReconnectingPTY spawns a PTY that reconnects using the token provided. It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON. Responses are PTY output that can be rendered.
(ctx context.Context, opts WorkspaceAgentReconnectingPTYOpts)
| 344 | // It communicates using `agent.ReconnectingPTYRequest` marshaled as JSON. |
| 345 | // Responses are PTY output that can be rendered. |
| 346 | func (c *Client) AgentReconnectingPTY(ctx context.Context, opts WorkspaceAgentReconnectingPTYOpts) (net.Conn, error) { |
| 347 | serverURL, err := c.client.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/pty", opts.AgentID)) |
| 348 | if err != nil { |
| 349 | return nil, xerrors.Errorf("parse url: %w", err) |
| 350 | } |
| 351 | q := serverURL.Query() |
| 352 | q.Set("reconnect", opts.Reconnect.String()) |
| 353 | q.Set("width", strconv.Itoa(int(opts.Width))) |
| 354 | q.Set("height", strconv.Itoa(int(opts.Height))) |
| 355 | q.Set("command", opts.Command) |
| 356 | if opts.Container != "" { |
| 357 | q.Set("container", opts.Container) |
| 358 | } |
| 359 | if opts.ContainerUser != "" { |
| 360 | q.Set("container_user", opts.ContainerUser) |
| 361 | } |
| 362 | if opts.BackendType != "" { |
| 363 | q.Set("backend_type", opts.BackendType) |
| 364 | } |
| 365 | // If we're using a signed token, set the query parameter. |
| 366 | if opts.SignedToken != "" { |
| 367 | q.Set(codersdk.SignedAppTokenQueryParameter, opts.SignedToken) |
| 368 | } |
| 369 | serverURL.RawQuery = q.Encode() |
| 370 | |
| 371 | // Shallow-clone the HTTP client so we never inherit a caller-provided |
| 372 | // cookie jar. Non-browser websocket auth uses the Coder-Session-Token |
| 373 | // header or a signed-token query param — never cookies. A stale jar |
| 374 | // cookie would take precedence on the server (cookies are checked |
| 375 | // before headers) and cause spurious 401s. |
| 376 | wsHTTPClient := *c.client.HTTPClient |
| 377 | wsHTTPClient.Jar = nil |
| 378 | |
| 379 | headers := http.Header{} |
| 380 | // If we're not using a signed token, set the session token header. |
| 381 | if opts.SignedToken == "" { |
| 382 | headers.Set(codersdk.SessionTokenHeader, c.client.SessionToken()) |
| 383 | } |
| 384 | //nolint:bodyclose |
| 385 | conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ |
| 386 | HTTPClient: &wsHTTPClient, |
| 387 | HTTPHeader: headers, |
| 388 | }) |
| 389 | if err != nil { |
| 390 | if res == nil { |
| 391 | return nil, err |
| 392 | } |
| 393 | return nil, codersdk.ReadBodyAsError(res) |
| 394 | } |
| 395 | return websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil |
| 396 | } |
| 397 | |
| 398 | func WithTestOnlyCoderContextResolver(ctx context.Context, r Resolver) context.Context { |
| 399 | return context.WithValue(ctx, dnsResolverContextKey{}, r) |
no test coverage detected