Path: blob/main/components/supervisor/cmd/terminal-attach.go
2498 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cmd56import (7"context"8"fmt"9"io"10"os"11"os/signal"12"syscall"13"time"1415"github.com/creack/pty"16"github.com/spf13/cobra"17"golang.org/x/term"1819"github.com/gitpod-io/gitpod/common-go/log"20"github.com/gitpod-io/gitpod/supervisor/api"21)2223var terminalAttachOpts struct {24Interactive bool25ForceResize bool26}2728var terminalAttachCmd = &cobra.Command{29Use: "attach <alias>",30Short: "attaches to a terminal",31Args: cobra.MaximumNArgs(1),32Run: func(cmd *cobra.Command, args []string) {33var (34alias string35client = api.NewTerminalServiceClient(dialSupervisor())36)37if len(args) == 0 {38ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)39defer cancel()40resp, err := client.List(ctx, &api.ListTerminalsRequest{})41if err != nil {42log.WithError(err).Fatal("cannot list terminals")43}44if len(resp.Terminals) == 0 {45log.Fatal("no terminal available")46}47if len(resp.Terminals) > 1 {48fmt.Fprintln(os.Stderr, "More than one terminal, please choose explicitly:")49for _, r := range resp.Terminals {50fmt.Fprintf(os.Stderr, "\t%s\n", r.Alias)51}52os.Exit(1)53}5455alias = resp.Terminals[0].Alias56} else {57alias = args[0]58}5960attachToTerminal(61context.Background(),62client,63alias,64attachToTerminalOpts{65ForceResize: terminalAttachOpts.ForceResize,66Interactive: terminalAttachOpts.Interactive,67},68)69},70}7172type attachToTerminalOpts struct {73Interactive bool74ForceResize bool75Token string76}7778func attachToTerminal(ctx context.Context, client api.TerminalServiceClient, alias string, opts attachToTerminalOpts) {79// Copy to stdout/stderr80listen, err := client.Listen(ctx, &api.ListenTerminalRequest{81Alias: alias,82})83if err != nil {84log.WithError(err).Fatal("cannot attach to terminal")85}86var exitCode int87errchan := make(chan error, 5)88go func() {89for {90resp, err := listen.Recv()91if err != nil {92errchan <- err93}94os.Stdout.Write(resp.GetData())95terminalExitCode := resp.GetExitCode()96if terminalExitCode > 0 {97exitCode = int(terminalExitCode)98}99}100}()101102// Set stdin in raw mode.103oldState, err := term.MakeRaw(int(os.Stdin.Fd()))104if err != nil {105panic(err)106}107defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.108109if opts.Interactive {110// Handle pty size.111ch := make(chan os.Signal, 1)112signal.Notify(ch, syscall.SIGWINCH)113go func() {114for range ch {115size, err := pty.GetsizeFull(os.Stdin)116if err != nil {117log.WithError(err).Error("cannot determine stdin's terminal size")118continue119}120121req := &api.SetTerminalSizeRequest{122Alias: alias,123Size: &api.TerminalSize{124Cols: uint32(size.Cols),125Rows: uint32(size.Rows),126WidthPx: uint32(size.X),127HeightPx: uint32(size.Y),128},129}130131var expectResize bool132if opts.ForceResize {133req.Priority = &api.SetTerminalSizeRequest_Force{Force: true}134expectResize = true135} else if opts.Token != "" {136req.Priority = &api.SetTerminalSizeRequest_Token{Token: opts.Token}137expectResize = true138}139140_, err = client.SetSize(ctx, req)141if err != nil && expectResize {142log.WithError(err).Error("cannot set terminal size")143continue144}145}146}()147ch <- syscall.SIGWINCH // Initial resize.148149// Copy stdin to the pty and the pty to stdout.150go func() {151buf := make([]byte, 32*1024)152for {153n, err := os.Stdin.Read(buf)154if n > 0 {155_, serr := client.Write(ctx, &api.WriteTerminalRequest{Alias: alias, Stdin: buf[:n]})156if serr != nil {157errchan <- err158return159}160}161if err != nil {162errchan <- err163return164}165}166}()167}168169// wait indefinitely170stopch := make(chan os.Signal, 1)171signal.Notify(stopch, syscall.SIGTERM|syscall.SIGINT)172select {173case err := <-errchan:174if err != io.EOF {175log.WithError(err).Error("error")176} else {177os.Exit(exitCode)178}179case <-stopch:180}181}182183func init() {184terminalCmd.AddCommand(terminalAttachCmd)185186terminalAttachCmd.Flags().BoolVarP(&terminalAttachOpts.Interactive, "internactive", "i", false, "assume control over the terminal")187terminalAttachCmd.Flags().BoolVarP(&terminalAttachOpts.ForceResize, "force-resize", "r", false, "force this terminal's size irregardless of other clients")188}189190191